From c9b0db17d46bcfe4a9620ab8cc759dbb11a211a5 Mon Sep 17 00:00:00 2001 From: Bob <160986752+bob2402@users.noreply.github.com> Date: Sat, 1 Jun 2024 14:57:02 +0800 Subject: [PATCH] display reactions (#475) --- app/UI/app.tsx | 106 ++++++++++++++++++++++++---- app/UI/app_update.tsx | 12 ++-- app/UI/dm.tsx | 3 +- app/UI/message-list.tsx | 66 ++++++++++++++++- app/UI/message-panel.tsx | 2 + app/UI/public-message-container.tsx | 3 +- app/database.ts | 31 ++++++++ deno.json | 3 +- libs/nostr.ts | 2 +- 9 files changed, 203 insertions(+), 25 deletions(-) diff --git a/app/UI/app.tsx b/app/UI/app.tsx index d632b31..35d1bd7 100644 --- a/app/UI/app.tsx +++ b/app/UI/app.tsx @@ -221,6 +221,39 @@ export class App { })); })(); + // Sync events since latest event in the database or beginning of time + { + const latestProfile = args.database.getLatestEvent(NostrKind.META_DATA); + const latestPublic = args.database.getLatestEvent(NostrKind.TEXT_NOTE); + const latestDeletion = args.database.getLatestEvent(NostrKind.DELETE); + const latestReaction = args.database.getLatestEvent(NostrKind.REACTION); + + // NOTE: + // 48 hours ago is because the latestEvent now does not distinguish space(relay). + // After adding space, it can be subscribed to as the most recent timestamp. + // So adding "hours ago" can at least have some content. + forever(sync_profile_events({ + database: args.database, + pool: args.pool, + since: latestProfile ? hours_ago(latestProfile.created_at, 48) : undefined, + })); + forever(sync_public_notes({ + pool: args.pool, + database: args.database, + since: latestPublic ? hours_ago(latestPublic.created_at, 48) : undefined, + })); + forever(sync_deletion_events({ + pool: args.pool, + database: args.database, + since: latestDeletion ? hours_ago(latestDeletion.created_at, 48) : undefined, + })); + forever(sync_reaction_events({ + pool: args.pool, + database: args.database, + since: latestReaction ? hours_ago(latestReaction.created_at, 48) : undefined, + })); + } + const app = new App( args.database, args.model, @@ -243,12 +276,9 @@ export class App { private initApp = async (installPrompt: InstallPrompt) => { console.log("App.initApp"); - // Sync events + // Sync event limit one { forever(sync_client_specific_data(this.pool, this.ctx, this.database)); - forever(sync_profile_events(this.database, this.pool)); - forever(sync_public_notes(this.pool, this.database)); - forever(sync_deletion_events(this.pool, this.database)); } (async () => { @@ -371,6 +401,7 @@ export class AppComponent extends Component @@ -398,6 +429,7 @@ export class AppComponent extends Component { +const sync_public_notes = async ( + args: { + pool: ConnectionPool; + database: Database_View; + since: number | undefined; + }, +) => { + const { pool, database, since } = args; const stream = await pool.newSub("sync_public_notes", { kinds: [NostrKind.TEXT_NOTE, NostrKind.Long_Form], - since: hours_ago(3), + since, }); if (stream instanceof Error) { return stream; @@ -613,12 +656,16 @@ const sync_client_specific_data = async ( }; const sync_deletion_events = async ( - pool: ConnectionPool, - database: Database_View, + args: { + pool: ConnectionPool; + database: Database_View; + since: number | undefined; + }, ) => { + const { pool, database, since } = args; const stream = await pool.newSub("sync_deletion_events", { kinds: [NostrKind.DELETE], - since: hours_ago(48), + since, }); if (stream instanceof Error) { return stream; @@ -639,6 +686,37 @@ const sync_deletion_events = async ( } }; -export function hours_ago(hours: number) { - return Math.floor(Date.now() / 1000) - hours * 60 * 60; +const sync_reaction_events = async ( + args: { + pool: ConnectionPool; + database: Database_View; + since: number | undefined; + }, +) => { + const { pool, database, since } = args; + const stream = await pool.newSub("sync_reaction_events", { + kinds: [NostrKind.REACTION], + since, + }); + if (stream instanceof Error) { + return stream; + } + for await (const msg of stream.chan) { + if (msg.res.type === "EOSE") { + continue; + } else if (msg.res.type === "NOTICE") { + console.log(`Notice: ${msg.res.note}`); + continue; + } + + const ok = await database.addEvent(msg.res.event, msg.url); + if (ok instanceof Error) { + console.error(msg); + console.error(ok); + } + } +}; + +export function hours_ago(time: number, hours: number) { + return time - hours * 60 * 60; } diff --git a/app/UI/app_update.tsx b/app/UI/app_update.tsx index a911635..5fe8409 100644 --- a/app/UI/app_update.tsx +++ b/app/UI/app_update.tsx @@ -169,18 +169,18 @@ const handle_update_event = async (chan: PutChannel, args: { continue; } - const current_relay = pool.getRelay(model.currentRelay); - if (current_relay == undefined) { - console.error(Array.from(pool.getRelays())); - continue; - } - const blowater_relay = pool.getRelay(default_blowater_relay); if (blowater_relay == undefined) { console.error(Array.from(pool.getRelays())); continue; } + let current_relay = pool.getRelay(model.currentRelay); + if (current_relay == undefined) { + current_relay = blowater_relay; + rememberCurrentRelay(default_blowater_relay); + } + // All events below are only valid after signning in if (event.type == "SelectRelay") { rememberCurrentRelay(event.relay.url); diff --git a/app/UI/dm.tsx b/app/UI/dm.tsx index 24102a5..2919006 100644 --- a/app/UI/dm.tsx +++ b/app/UI/dm.tsx @@ -17,7 +17,7 @@ import { NewMessageChecker, PinListGetter, } from "./conversation-list.tsx"; -import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx"; +import { func_GetEventByID, func_GetReactionsByEventID, func_IsAdmin } from "./message-list.tsx"; export type DM_Model = { currentConversation: PublicKey | undefined; @@ -37,6 +37,7 @@ type DirectMessageContainerProps = { isUserBlocked: (pubkey: PublicKey) => boolean; getEventByID: func_GetEventByID; isAdmin: func_IsAdmin | undefined; + getReactionsByEventID: func_GetReactionsByEventID; }; userBlocker: UserBlocker; } & DM_Model; diff --git a/app/UI/message-list.tsx b/app/UI/message-list.tsx index ce0c51a..f99ebbd 100644 --- a/app/UI/message-list.tsx +++ b/app/UI/message-list.tsx @@ -55,6 +55,7 @@ interface Props { relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; isAdmin: func_IsAdmin | undefined; + getReactionsByEventID: func_GetReactionsByEventID; }; } @@ -282,6 +283,8 @@ export type func_GetEventByID = ( id: string | NoteID, ) => Parsed_Event | undefined; +export type func_GetReactionsByEventID = (id: string) => Set; + function MessageBoxGroup(props: { authorProfile: ProfileData | undefined; messages: ChatMessage[]; @@ -300,6 +303,7 @@ function MessageBoxGroup(props: { relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; isAdmin: func_IsAdmin | undefined; + getReactionsByEventID: func_GetReactionsByEventID; }; }) { const first_message = props.messages[0]; @@ -353,6 +357,10 @@ function MessageBoxGroup(props: { props.getters, )} + , @@ -374,7 +382,7 @@ function MessageBoxGroup(props: { })} {Time(msg.created_at)}
{ParseMessageContent(msg, props.emit, props.getters)} +
, ); @@ -579,3 +591,55 @@ function ReplyTo( ); } + +function Reactions( + props: { + myPublicKey: PublicKey; + events: Set; + }, +) { + const reactions: Map = new Map(); + for (const event of props.events) { + let reaction = event.content; + if (!reaction) continue; + if (reaction === "+") reaction = "👍"; + if (reaction === "-") reaction = "👎"; + + const pre = reactions.get(reaction); + const isClicked = event.pubkey === props.myPublicKey.hex; + if (pre) { + reactions.set(reaction, { + count: pre.count + 1, + clicked: pre.clicked || isClicked, + }); + } else { + reactions.set(reaction, { + count: 1, + clicked: isClicked, + }); + } + } + return ( + reactions.size > 0 + ? ( +
+ {Array.from(reactions).map(([reaction, { count, clicked }]) => { + return ( +
+
{reaction}
+ {count > 1 &&
{` ${count}`}
} +
+ ); + })} +
+ ) + : null + ); +} diff --git a/app/UI/message-panel.tsx b/app/UI/message-panel.tsx index dfa8168..b6d679a 100644 --- a/app/UI/message-panel.tsx +++ b/app/UI/message-panel.tsx @@ -26,6 +26,7 @@ import { BlockUser, UnblockUser } from "./user-detail.tsx"; import { DeleteEvent, func_GetEventByID, + func_GetReactionsByEventID, func_IsAdmin, MessageList, ReplyToMessage, @@ -75,6 +76,7 @@ interface MessagePanelProps { isUserBlocked: (pubkey: PublicKey) => boolean; getEventByID: func_GetEventByID; isAdmin: func_IsAdmin | undefined; + getReactionsByEventID: func_GetReactionsByEventID; }; } diff --git a/app/UI/public-message-container.tsx b/app/UI/public-message-container.tsx index 663a573..16f5583 100644 --- a/app/UI/public-message-container.tsx +++ b/app/UI/public-message-container.tsx @@ -13,7 +13,7 @@ import { NostrAccountContext } from "../../libs/nostr.ts/nostr.ts"; import { MessagePanel } from "./message-panel.tsx"; import { PublicKey } from "../../libs/nostr.ts/key.ts"; import { ChatMessage } from "./message.ts"; -import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx"; +import { func_GetEventByID, func_GetReactionsByEventID, func_IsAdmin } from "./message-list.tsx"; import { Filter, FilterContent } from "./filter.tsx"; import { NoteID } from "../../libs/nostr.ts/nip19.ts"; @@ -38,6 +38,7 @@ type Props = { isUserBlocked: func_IsUserBlocked; getEventByID: func_GetEventByID; isAdmin: func_IsAdmin | undefined; + getReactionsByEventID: func_GetReactionsByEventID; }; } & Public_Model; diff --git a/app/database.ts b/app/database.ts index e1fa096..54fc38a 100644 --- a/app/database.ts +++ b/app/database.ts @@ -66,6 +66,11 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover private readonly caster = csp.multi<{ event: Parsed_Event; relay?: string }>(this.sourceOfChange); private readonly profiles = new Map(); private readonly deletionEvents = new Map(); + private readonly reactionEvents = new Map< + /* event id */ string, + /* reaction events */ Set + >(); + private readonly latestEvents = new Map(); private constructor( private readonly eventsAdapter: EventsAdapter, @@ -126,6 +131,7 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover new Set(all_removed_events.map((mark) => mark.event_id)), ); console.log("Datebase_View:New time spent", Date.now() - t); + for (const event of db.events.values()) { if (event.kind == NostrKind.META_DATA) { // @ts-ignore @@ -139,9 +145,21 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover event.parsedTags.e.forEach((event_id) => { db.deletionEvents.set(event_id, event); }); + } else if (event.kind == NostrKind.REACTION) { + const eventId = event.parsedTags.e[0]; + const events = db.reactionEvents.get(event.parsedTags.e[0]) || new Set(); + events.add(event); + db.reactionEvents.set(eventId, events); + } + + // update latest event + const preLatest = db.latestEvents.get(event.kind); + if (preLatest === undefined || preLatest.created_at < event.created_at) { + db.latestEvents.set(event.kind, event); } } console.log(`Datebase_View:Deletion events size: ${db.deletionEvents.size}`); + console.log(`Datebase_View:Reaction events size: ${db.reactionEvents.size}`); return db; } @@ -164,6 +182,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover } } + getLatestEvent = (kind: NostrKind) => { + return this.latestEvents.get(kind); + }; + isDeleted(id: string, admin?: string) { const deletionEvent = this.deletionEvents.get(id); if (deletionEvent == undefined) { @@ -177,6 +199,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover deletionEvent.pubkey == admin; } + getReactionEvents = (id: string) => { + return this.reactionEvents.get(id) || new Set(); + }; + async remove(id: string): Promise { this.removedEvents.add(id); await this.eventMarker.markEvent(id, "removed"); @@ -296,6 +322,11 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover parsedEvent.parsedTags.e.forEach((event_id) => { this.deletionEvents.set(event_id, parsedEvent); }); + } else if (parsedEvent.kind == NostrKind.REACTION) { + const eventId = parsedEvent.parsedTags.e[0]; + const events = this.reactionEvents.get(eventId) || new Set(); + events.add(parsedEvent); + this.reactionEvents.set(eventId, events); } await this.eventsAdapter.put(event); diff --git a/deno.json b/deno.json index 233db00..4851d16 100644 --- a/deno.json +++ b/deno.json @@ -27,7 +27,8 @@ "vendor", "*cov_profile*", "*tauri*", - "app/UI/assets/" + "app/UI/assets/", + "*/tailwind.js" ], "indentWidth": 4, "lineWidth": 110 diff --git a/libs/nostr.ts b/libs/nostr.ts index 98b715e..bc371c4 160000 --- a/libs/nostr.ts +++ b/libs/nostr.ts @@ -1 +1 @@ -Subproject commit 98b715e219dfe2a956736e239e69dc346dec1301 +Subproject commit bc371c443d9846be0d3caec17a1e24d93cd4e6a0