From f778596fbcb941b681f8cb40050829031ad2b7b6 Mon Sep 17 00:00:00 2001 From: Franz Geffke Date: Fri, 1 Sep 2023 22:36:31 +0100 Subject: [PATCH] client-web: added new following feed; performance improvements --- .../src/components/create-event-form.tsx | 55 +---- client-web/src/components/event.tsx | 8 +- client-web/src/defaults.ts | 2 +- client-web/src/layouts/primary.tsx | 7 + client-web/src/main.tsx | 2 + client-web/src/routes/following-feed.tsx | 80 ++++++++ client-web/src/routes/profile.tsx | 1 + client-web/src/routes/welcome.tsx | 15 +- client-web/src/state/client.ts | 133 +----------- client-web/src/state/worker-types.ts | 2 + client-web/src/state/worker.ts | 190 ++++++++++++++++-- 11 files changed, 301 insertions(+), 194 deletions(-) create mode 100644 client-web/src/routes/following-feed.tsx diff --git a/client-web/src/components/create-event-form.tsx b/client-web/src/components/create-event-form.tsx index 25a3089..34ab47a 100644 --- a/client-web/src/components/create-event-form.tsx +++ b/client-web/src/components/create-event-form.tsx @@ -55,67 +55,35 @@ export const CreateEventForm = () => { ] ); + const [eventContent, setEventContent] = useState(""); const [relayUrls, setRelayUrls] = useState([]); + const publicKey = useRef(undefined); const toast = useToast(); - const currentPubkey = useRef(undefined); - useEffect(() => { const loadUser = async () => { if ( - newEventName !== "NewShortTextNoteResponse" - // newEventName !== "NewQuoteRepost" + !publicKeyTags || + newEventName !== "NewShortTextNoteResponse" || + publicKeyTags[0][1] === publicKey.current ) { return; } - const publicKey = useNClient.getState().newEvent?.pubkey; - if (!publicKey) { - return; - } - if (currentPubkey.current === publicKey) { - return; - } - currentPubkey.current = publicKey; - - const foundUsers: { - user: NUser; - relayUrls: string[]; - }[] = []; const foundRelayUrls = []; if (publicKeyTags) { + publicKey.current = publicKeyTags[0][1] || undefined; for (const tags of publicKeyTags) { - let relayUrl; + // let relayUrl; /** * Get the relay url from the tags [p, pubkey, relayUrl] */ if (tags.length === 2) { - relayUrl = tags[1]; foundRelayUrls.push(tags[1]); } - const key = tags[0]; - const record = await useNClient.getState().getUser(key); - if (record) { - foundUsers.push({ - user: record.user, - relayUrls: record.relayUrls - ? record.relayUrls - : relayUrl - ? [relayUrl] - : [], - }); - } else { - foundUsers.push({ - user: new NUser({ pubkey: key }), - relayUrls: relayUrl ? [relayUrl] : [], - }); - } } } - if (foundUsers.length > 0) { - setUsers(foundUsers); - } if (foundRelayUrls.length > 0) { setRelayUrls(foundRelayUrls); } @@ -163,7 +131,7 @@ export const CreateEventForm = () => { return; } - if (!newEvent.content) { + if (eventContent === "") { setErrors(["Event content is required"]); toast({ title: "Error", @@ -176,6 +144,7 @@ export const CreateEventForm = () => { } try { + useNClient.getState().setNewEventContent(eventContent); const evId = await useNClient.getState().signAndSendEvent({ event: newEvent, relayUrls, @@ -299,10 +268,8 @@ export const CreateEventForm = () => { - useNClient.getState().setNewEventContent(e.target.value) - } + value={eventContent} + onChange={(e) => setEventContent(e.target.value)} placeholder="Enter event content" /> diff --git a/client-web/src/components/event.tsx b/client-web/src/components/event.tsx index 2ced246..1ba0ed1 100644 --- a/client-web/src/components/event.tsx +++ b/client-web/src/components/event.tsx @@ -425,10 +425,10 @@ export function Event({ {reposts && ( <> Reposts - {reposts.map((r) => { + {reposts.map((r, index) => { const user = r.user ? r.user : { pubkey: r.event.pubkey }; return ( - + Mentions - {mentions.map((u) => ( - + {mentions.map((u, index) => ( + } /> */} + } + /> + { @@ -34,6 +35,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render( } /> } /> } /> + } /> } /> } /> } /> diff --git a/client-web/src/routes/following-feed.tsx b/client-web/src/routes/following-feed.tsx new file mode 100644 index 0000000..6b7838b --- /dev/null +++ b/client-web/src/routes/following-feed.tsx @@ -0,0 +1,80 @@ +import { Heading, Box, Text, Grid } from "@chakra-ui/react"; +import { NEVENT_KIND, NFilters } from "@nostr-ts/common"; +import { useEffect } from "react"; +import { useNClient } from "../state/client"; +import { Events } from "../components/events"; +import { MAX_EVENTS } from "../defaults"; +import { CreateEventForm } from "../components/create-event-form"; +import { User } from "../components/user"; + +export function FollowingFeedRoute() { + const [connected, eventsEqualOrMoreThanMax, followingUserIds] = useNClient( + (state) => [ + state.connected, + state.events.length >= state.maxEvents, + state.followingUserIds, + ] + ); + const filters = new NFilters({ + limit: MAX_EVENTS, + authors: followingUserIds, + kinds: [NEVENT_KIND.SHORT_TEXT_NOTE, NEVENT_KIND.LONG_FORM_CONTENT], + }); + + const view = `following-feed`; + + const init = async () => { + if (!connected) return; + + await useNClient.getState().setMaxEvents(MAX_EVENTS); + await useNClient.getState().clearEvents(); + await useNClient.getState().setViewSubscription(view, filters); + }; + + /** + * Handle initial load + */ + useEffect(() => { + init(); + }, []); + + /** + * Remove subscription when we hit the limit + */ + useEffect(() => { + const remove = async () => { + if (!connected) return; + await useNClient.getState().removeViewSubscription(view); + }; + + if (eventsEqualOrMoreThanMax) { + remove(); + } + }, [eventsEqualOrMoreThanMax]); + + return ( + + + + {connected ? ( + + ) : ( + Not connected. + )} + + + + + + Broadcast to the Network + + + + + ); +} diff --git a/client-web/src/routes/profile.tsx b/client-web/src/routes/profile.tsx index caf4efe..f6adf9f 100644 --- a/client-web/src/routes/profile.tsx +++ b/client-web/src/routes/profile.tsx @@ -36,6 +36,7 @@ export function ProfileRoute() { // USER if (!userRecord || userRecord.user.pubkey !== pubkey) { + await useNClient.getState().setMaxEvents(MAX_EVENTS); await useNClient.getState().clearEvents(); await useNClient.getState().setViewSubscription(view, defaultFilters); diff --git a/client-web/src/routes/welcome.tsx b/client-web/src/routes/welcome.tsx index ca7abba..4d82720 100644 --- a/client-web/src/routes/welcome.tsx +++ b/client-web/src/routes/welcome.tsx @@ -22,6 +22,7 @@ export function WelcomeRoute() { const init = async () => { if (!connected || initDone.current) return; initDone.current = true; + await useNClient.getState().setMaxEvents(MAX_EVENTS); await useNClient.getState().clearEvents(); await useNClient.getState().setViewSubscription("welcome", defaultFilters); }; @@ -60,12 +61,14 @@ export function WelcomeRoute() { {connected ? ( - + + + ) : ( About Nostr diff --git a/client-web/src/state/client.ts b/client-web/src/state/client.ts index 4a25240..d067d97 100644 --- a/client-web/src/state/client.ts +++ b/client-web/src/state/client.ts @@ -70,7 +70,7 @@ export const useNClient = create((set, get) => ({ await get().store.init(config); const processEvents = (events: Event[]) => { - events.forEach((event) => { + events.map((event) => { const payload = event.data; if (payload.type === "event:new" || payload.type === "event:update") { @@ -585,129 +585,16 @@ export const useNClient = create((set, get) => ({ }, }); - const process = async () => { - const eventUserPubkeys: { - pubkey: string; - relayUrls: string[]; - }[] = []; - - // const eventIds: { - // id: string; - // relayUrls: string[]; - // }[] = []; - - const relEventIds: { - id: string; - relayUrls: string[]; - }[] = []; - - for (const ev of get().events) { - // TODO: Check if user is stale - if (ev.event?.pubkey && !ev.user?.pubkey) { - eventUserPubkeys.push({ - pubkey: ev.event.pubkey, - relayUrls: ev.eventRelayUrls, - }); - } - // if (!ev.inResponseTo) { - // const tags = eventHasEventTags(ev.event); - // if (tags) { - // for (const tag of tags) { - // if (tag.marker === "root") { - // eventIds.push({ - // id: tag.eventId, - // relayUrls: tag.relayUrl ? [tag.relayUrl] : ev.eventRelayUrls, - // }); - // } - // } - // } - // } - if (!ev.reactions) { - relEventIds.push({ - id: ev.event.id, - relayUrls: ev.eventRelayUrls, - }); - } - } - - const relayUrlToPubkeysMap: Record> = {}; - - for (const ev of eventUserPubkeys) { - for (const relayUrl of ev.relayUrls) { - if (!relayUrlToPubkeysMap[relayUrl]) { - relayUrlToPubkeysMap[relayUrl] = new Set(); - } - relayUrlToPubkeysMap[relayUrl].add(ev.pubkey); - } - } - - const reqUsers: RelaysWithIdsOrKeys[] = Object.entries( - relayUrlToPubkeysMap - ).map(([relayUrl, pubkeysSet]) => { - return { - source: "users", - relayUrl, - idsOrKeys: [...pubkeysSet], - }; - }); - - // const relayUrlToEventIdsMap: Record> = {}; - - // for (const ev of eventIds) { - // for (const relayUrl of ev.relayUrls) { - // if (!relayUrlToEventIdsMap[relayUrl]) { - // relayUrlToEventIdsMap[relayUrl] = new Set(); - // } - // relayUrlToEventIdsMap[relayUrl].add(ev.id); - // } - // } - - // const reqEvents: RelaysWithIdsOrKeys[] = Object.entries( - // relayUrlToEventIdsMap - // ).map(([relayUrl, eventIdsSet]) => { - // return { - // source: "events", - // relayUrl, - // idsOrKeys: [...eventIdsSet], - // }; - // }); - - // This map will keep track of relayUrls and their associated eventIds. - const relayUrlToRelEventIdsMap: Record> = {}; - - for (const ev of relEventIds) { - for (const relayUrl of ev.relayUrls) { - if (!relayUrlToRelEventIdsMap[relayUrl]) { - relayUrlToRelEventIdsMap[relayUrl] = new Set(); - } - relayUrlToRelEventIdsMap[relayUrl].add(ev.id); - } - } - - const reqRelEvents: RelaysWithIdsOrKeys[] = Object.entries( - relayUrlToRelEventIdsMap - ).map(([relayUrl, eventIdsSet]) => { - return { - source: "events:related", - relayUrl, - idsOrKeys: [...eventIdsSet], - }; - }); - - const infoRequestPromises = []; - for (const item of [...reqUsers, ...reqRelEvents]) { - infoRequestPromises.push( - await get().requestInformation(item, { - timeoutIn: 60000, - view, - }) - ); - } - }; - // TODO: This is not accurate - setTimeout(process, 1000); - setTimeout(process, 6000); + setTimeout(async () => { + await get().store.processActiveEvents(view); + }, 1500); + setTimeout(async () => { + await get().store.processActiveEvents(view); + }, 6000); + setTimeout(async () => { + await get().store.processActiveEvents(view); + }, 12000); }, /** diff --git a/client-web/src/state/worker-types.ts b/client-web/src/state/worker-types.ts index 14c4c52..49b18c8 100644 --- a/client-web/src/state/worker-types.ts +++ b/client-web/src/state/worker-types.ts @@ -34,6 +34,8 @@ export interface NClientWorker extends NClientBase { } ) => void; + processActiveEvents: (view: string) => void; + /** * Set to disconnect state * - Clears all subscriptions diff --git a/client-web/src/state/worker.ts b/client-web/src/state/worker.ts index 1aefe50..c14549e 100644 --- a/client-web/src/state/worker.ts +++ b/client-web/src/state/worker.ts @@ -375,7 +375,7 @@ class WorkerClass implements NClientWorker { const mentions = newEvent.event.hasMentions(); if (mentions) { - for (const mention of mentions) { + mentions.map(async (mention) => { const user = await this.getUser(mention); if (user) { if (newEvent.mentions) { @@ -390,7 +390,7 @@ class WorkerClass implements NClientWorker { newEvent.mentions = [new NUserBase({ pubkey: mention })]; } } - } + }); } // Check if event is a response to another event @@ -501,8 +501,37 @@ class WorkerClass implements NClientWorker { } for (const item of this.eventsMap.values()) { + let changed = false; if (item.event.pubkey === newUser.pubkey) { item.user = newUser; + changed = true; + } + if (item.reactions) { + item.reactions.map((reaction) => { + if (reaction.event.pubkey === newUser.pubkey) { + reaction.user = newUser; + changed = true; + } + }); + } + if (item.replies) { + item.replies.map((reply) => { + if (reply.event.pubkey === newUser.pubkey) { + reply.user = newUser; + changed = true; + } + }); + } + if (item.mentions) { + item.mentions.map((mention) => { + if (mention.pubkey === newUser.pubkey) { + mention = newUser; + changed = true; + } + }); + } + + if (changed) { this.updateEvent(item); } } @@ -560,7 +589,7 @@ class WorkerClass implements NClientWorker { .filter((tag) => tag.eventId) .map((tag) => tag.eventId); - for (const id of eventIds) { + eventIds.map((id) => { const event = this.eventsMap.get(id); if (event) { if (event.reposts) { @@ -579,7 +608,7 @@ class WorkerClass implements NClientWorker { console.log(`Repost event added to event ${event.event.id}`); this.updateEvent(event); } - } + }); }); } } @@ -620,11 +649,11 @@ class WorkerClass implements NClientWorker { // TODO: This is bad this.addQueueItems(result); - for (const item of result) { + result.map((item) => { this.updateQueueItem({ ...item, }); - } + }); return result; } else { throw new Error("Failed to send event"); @@ -638,11 +667,11 @@ class WorkerClass implements NClientWorker { this.addQueueItems(payload); const result = this.client.sendQueueItems(payload); if (result) { - for (const item of result) { + result.map((item) => { this.updateQueueItem({ ...item, }); - } + }); return result; } else { throw new Error("Failed to send event"); @@ -679,8 +708,8 @@ class WorkerClass implements NClientWorker { user: newFollowing, relayUrls, }); - for (const relayUrl of relayUrls) { - await this.requestInformation( + relayUrls.map((relayUrl) => { + this.requestInformation( { source: "users", idsOrKeys: [pubkey], @@ -690,7 +719,7 @@ class WorkerClass implements NClientWorker { timeoutIn: 10000, } ); - } + }); } this.followingUserIds.push(pubkey); } @@ -742,6 +771,135 @@ class WorkerClass implements NClientWorker { } } + async processActiveEvents(view: string) { + const eventUserPubkeys: { + pubkey: string; + relayUrls: string[]; + }[] = []; + + // const eventIds: { + // id: string; + // relayUrls: string[]; + // }[] = []; + + const relEventIds: { + id: string; + relayUrls: string[]; + }[] = []; + + for (const entry of this.eventsMap.entries()) { + const ev = entry[1]; + // TODO: Check if user is stale + if (ev.event?.pubkey && !ev.user?.pubkey) { + eventUserPubkeys.push({ + pubkey: ev.event.pubkey, + relayUrls: ev.eventRelayUrls, + }); + } + // if (!ev.inResponseTo) { + // const tags = eventHasEventTags(ev.event); + // if (tags) { + // for (const tag of tags) { + // if (tag.marker === "root") { + // eventIds.push({ + // id: tag.eventId, + // relayUrls: tag.relayUrl ? [tag.relayUrl] : ev.eventRelayUrls, + // }); + // } + // } + // } + // } + if (!ev.reactions) { + relEventIds.push({ + id: ev.event.id, + relayUrls: ev.eventRelayUrls, + }); + } else { + ev.reactions.map((reaction) => { + if (!reaction.user?.data) { + eventUserPubkeys.push({ + pubkey: reaction.event.pubkey, + relayUrls: ev.eventRelayUrls, + }); + } + }); + } + if (ev.replies) { + ev.replies.map((reply) => { + if (!reply.user?.data) { + eventUserPubkeys.push({ + pubkey: reply.event.pubkey, + relayUrls: ev.eventRelayUrls, + }); + } + }); + } + if (ev.mentions) { + ev.mentions.map((mention) => { + if (!mention.data) { + eventUserPubkeys.push({ + pubkey: mention.pubkey, + relayUrls: ev.eventRelayUrls, + }); + } + }); + } + } + + const relayUrlToPubkeysMap: Record> = {}; + + eventUserPubkeys.map((ev) => { + for (const relayUrl of ev.relayUrls) { + if (!relayUrlToPubkeysMap[relayUrl]) { + relayUrlToPubkeysMap[relayUrl] = new Set(); + } + relayUrlToPubkeysMap[relayUrl].add(ev.pubkey); + } + }); + + const reqUsers: RelaysWithIdsOrKeys[] = Object.entries( + relayUrlToPubkeysMap + ).map(([relayUrl, pubkeysSet]) => { + return { + source: "users", + relayUrl, + idsOrKeys: [...pubkeysSet], + }; + }); + + // This map will keep track of relayUrls and their associated eventIds. + const relayUrlToRelEventIdsMap: Record> = {}; + + relEventIds.map((ev) => { + for (const relayUrl of ev.relayUrls) { + if (!relayUrlToRelEventIdsMap[relayUrl]) { + relayUrlToRelEventIdsMap[relayUrl] = new Set(); + } + relayUrlToRelEventIdsMap[relayUrl].add(ev.id); + } + }); + + const reqRelEvents: RelaysWithIdsOrKeys[] = Object.entries( + relayUrlToRelEventIdsMap + ).map(([relayUrl, eventIdsSet]) => { + return { + source: "events:related", + relayUrl, + idsOrKeys: [...eventIdsSet], + }; + }); + + const infoRequestPromises = []; + [...reqUsers, ...reqRelEvents].map((item) => { + infoRequestPromises.push( + this.requestInformation(item, { + timeoutIn: 60000, + view, + }) + ); + }); + } + async requestInformation( payload: RelaysWithIdsOrKeys, options: SubscriptionOptions @@ -830,10 +988,10 @@ class WorkerClass implements NClientWorker { return undefined; } - const subIds = []; + const subIds: string[] = []; if (kinds) { - for (const id of eventIds) { + eventIds.map((id) => { const subscription = subscriptions.find( (sub) => sub.filters && @@ -843,16 +1001,16 @@ class WorkerClass implements NClientWorker { if (subscription) { subIds.push(subscription.id); } - } + }); } else { - for (const id of eventIds) { + eventIds.map((id) => { const subscription = subscriptions.find( (sub) => sub.filters && sub.filters["#e"]?.includes(id) ); if (subscription) { subIds.push(subscription.id); } - } + }); } return subIds.length > 0 ? subIds : undefined;