From 049b2d56c899832dc12bb0e5c3b3aaf2c96f61c4 Mon Sep 17 00:00:00 2001 From: Bob <160986752+bob2402@users.noreply.github.com> Date: Tue, 14 May 2024 17:52:02 +0800 Subject: [PATCH] sync deletion events (#455) --- app/UI/app.tsx | 72 +++++++++++++++++++++++++++-- app/UI/app_model.ts | 3 -- app/UI/app_update.tsx | 26 +++++++++-- app/UI/dm.tsx | 5 +- app/UI/icons/delete-icon.tsx | 17 ++++--- app/UI/message-list.tsx | 70 ++++++++++++++++++++++++---- app/UI/message-panel.tsx | 11 ++++- app/UI/nav.tsx | 12 +++-- app/UI/public-message-container.tsx | 5 +- app/UI/relay-switch-list.tsx | 2 +- app/UI/setting.tsx | 5 +- app/database.ts | 30 ++++++++++-- 12 files changed, 212 insertions(+), 46 deletions(-) diff --git a/app/UI/app.tsx b/app/UI/app.tsx index c2fb475..d632b31 100644 --- a/app/UI/app.tsx +++ b/app/UI/app.tsx @@ -29,12 +29,13 @@ import { PublicMessageContainer } from "./public-message-container.tsx"; import { ChatMessage } from "./message.ts"; import { filter, forever, map } from "./_helper.ts"; import { RightPanel } from "./components/right-panel.tsx"; -import { ComponentChildren } from "https://esm.sh/preact@10.17.1"; import { SignIn } from "./sign-in.tsx"; import { getTags, Parsed_Event } from "../nostr.ts"; import { Toast } from "./components/toast.tsx"; import { ToastChannel } from "./components/toast.tsx"; import { RightPanelChannel } from "./components/right-panel.tsx"; +import { getRelayInformation } from "./relay-detail.tsx"; +import { func_IsAdmin } from "./message-list.tsx"; export async function Start(database: DexieDatabase) { console.log("Start the application"); @@ -56,6 +57,7 @@ export async function Start(database: DexieDatabase) { const toastInputChan: ToastChannel = new Channel(); const dbView = await Database_View.New(database, database, database); const newNostrEventChannel = new Channel(); + (async () => { for await (const event of newNostrEventChannel) { const err = await pool.sendEvent(event); @@ -246,6 +248,7 @@ export class App { 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 () => { @@ -315,7 +318,24 @@ type AppProps = { installPrompt: InstallPrompt; }; -export class AppComponent extends Component { +export class AppComponent extends Component { + state = { + isAdmin: undefined, + admin: undefined, + }; + + async componentDidMount() { + await this.updateAdminState(); + for await (const update of this.props.eventBus.onChange()) { + if (update.type == "SelectRelay") { + this.updateAdminState(); + } + } + } + render(props: AppProps) { const t = Date.now(); const model = props.model; @@ -350,6 +370,7 @@ export class AppComponent extends Component { getProfilesByText: app.database.getProfilesByText, isUserBlocked: app.conversationLists.isUserBlocked, getEventByID: app.database.getEventByID, + isAdmin: this.state.isAdmin, }} userBlocker={app.conversationLists} /> @@ -376,6 +397,7 @@ export class AppComponent extends Component { getProfilesByText: app.database.getProfilesByText, isUserBlocked: app.conversationLists.isUserBlocked, getEventByID: app.database.getEventByID, + isAdmin: this.state.isAdmin, }} messages={Array.from( map( @@ -386,7 +408,8 @@ export class AppComponent extends Component { return false; } const relays = app.database.getRelayRecord(e.id); - return relays.has(model.currentRelay); + return relays.has(model.currentRelay) && + !app.database.isDeleted(e.id, this.state.admin); }, ), (e) => { @@ -464,6 +487,22 @@ export class AppComponent extends Component { console.debug("AppComponent:end", Date.now() - t); return final; } + + updateAdminState = async () => { + const currentRelayInformation = await getRelayInformation(this.props.model.currentRelay); + if (currentRelayInformation instanceof Error) { + console.error(currentRelayInformation); + return; + } + this.setState({ + admin: currentRelayInformation.pubkey, + isAdmin: this.isAdmin(currentRelayInformation.pubkey), + }); + }; + + isAdmin = (admin?: string) => (pubkey: string) => { + return admin === pubkey; + }; } // todo: move to somewhere else @@ -573,6 +612,33 @@ const sync_client_specific_data = async ( } }; +const sync_deletion_events = async ( + pool: ConnectionPool, + database: Database_View, +) => { + const stream = await pool.newSub("sync_deletion_events", { + kinds: [NostrKind.DELETE], + since: hours_ago(48), + }); + 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(hours: number) { return Math.floor(Date.now() / 1000) - hours * 60 * 60; } diff --git a/app/UI/app_model.ts b/app/UI/app_model.ts index dff8410..6d882e0 100644 --- a/app/UI/app_model.ts +++ b/app/UI/app_model.ts @@ -1,10 +1,7 @@ import { NavigationModel } from "./nav.tsx"; -import { ProfileData } from "../features/profile.ts"; - import { DM_Model } from "./dm.tsx"; import { Public_Model } from "./public-message-container.tsx"; import { App } from "./app.tsx"; -import { PublicKey } from "../../libs/nostr.ts/key.ts"; import { default_blowater_relay } from "./relay-config.ts"; export type Model = { diff --git a/app/UI/app_update.tsx b/app/UI/app_update.tsx index 2d90c21..8ba456c 100644 --- a/app/UI/app_update.tsx +++ b/app/UI/app_update.tsx @@ -5,7 +5,7 @@ import { PutChannel, sleep, } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; -import { prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts"; +import { prepareDeletionEvent, prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts"; import { prepareReplyEvent } from "../nostr.ts"; import { PublicKey } from "../../libs/nostr.ts/key.ts"; import { NoteID } from "../../libs/nostr.ts/nip19.ts"; @@ -55,7 +55,7 @@ import { SendingEventRejection, ToastChannel } from "./components/toast.tsx"; import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts"; import { default_blowater_relay } from "./relay-config.ts"; import { forever } from "./_helper.ts"; -import { func_GetEventByID } from "./message-list.tsx"; +import { DeleteEvent, func_GetEventByID } from "./message-list.tsx"; import { FilterContent } from "./filter.tsx"; import { CloseRightPanel } from "./components/right-panel.tsx"; import { RightPanelChannel } from "./components/right-panel.tsx"; @@ -86,7 +86,8 @@ export type UI_Interaction_Event = | FilterContent | CloseRightPanel | ReplyToMessage - | EditorSelectProfile; + | EditorSelectProfile + | DeleteEvent; type BackToContactList = { type: "BackToContactList"; @@ -317,6 +318,25 @@ const handle_update_event = async (chan: PutChannel, args: { app.rightPanelInputChan.put(undefined); } // // + // Deletion + // + else if (event.type == "DeleteEvent") { + const deletionEvent = await prepareDeletionEvent( + app.ctx, + "Request deletion", + event.event, + ); + if (deletionEvent instanceof Error) { + app.toastInputChan.put(() => deletionEvent.message); + continue; + } + const err = await current_relay.sendEvent(deletionEvent); + if (err instanceof Error) { + app.toastInputChan.put(() => err.message); + continue; + } + } // + // // DM // else if (event.type == "ViewUserDetail") { diff --git a/app/UI/dm.tsx b/app/UI/dm.tsx index bf23efe..24102a5 100644 --- a/app/UI/dm.tsx +++ b/app/UI/dm.tsx @@ -9,7 +9,7 @@ import { IconButtonClass } from "./components/tw.ts"; import { LeftArrowIcon } from "./icons/left-arrow-icon.tsx"; import { MessagePanel_V0 } from "./message-panel.tsx"; -import { func_GetProfileByPublicKey, func_GetProfilesByText, ProfileGetter } from "./search.tsx"; +import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx"; import { ConversationList, @@ -17,7 +17,7 @@ import { NewMessageChecker, PinListGetter, } from "./conversation-list.tsx"; -import { func_GetEventByID } from "./message-list.tsx"; +import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx"; export type DM_Model = { currentConversation: PublicKey | undefined; @@ -36,6 +36,7 @@ type DirectMessageContainerProps = { relayRecordGetter: RelayRecordGetter; isUserBlocked: (pubkey: PublicKey) => boolean; getEventByID: func_GetEventByID; + isAdmin: func_IsAdmin | undefined; }; userBlocker: UserBlocker; } & DM_Model; diff --git a/app/UI/icons/delete-icon.tsx b/app/UI/icons/delete-icon.tsx index add8c9e..f61ef62 100644 --- a/app/UI/icons/delete-icon.tsx +++ b/app/UI/icons/delete-icon.tsx @@ -13,18 +13,17 @@ export function DeleteIcon(props: { - - - + ); } diff --git a/app/UI/message-list.tsx b/app/UI/message-list.tsx index 08404b1..f6a581f 100644 --- a/app/UI/message-list.tsx +++ b/app/UI/message-list.tsx @@ -29,23 +29,32 @@ import { AboutIcon } from "./icons/about-icon.tsx"; import { BackgroundColor_MessagePanel, PrimaryTextColor } from "./style/colors.ts"; import { Parsed_Event } from "../nostr.ts"; import { NoteID } from "../../libs/nostr.ts/nip19.ts"; -import { robohash } from "./relay-detail.tsx"; +import { RelayInformation, robohash } from "./relay-detail.tsx"; import { ReplyIcon } from "./icons/reply-icon.tsx"; import { ChatMessagesGetter } from "./app_update.tsx"; import { NostrKind } from "../../libs/nostr.ts/nostr.ts"; import { func_GetProfileByPublicKey } from "./search.tsx"; +import { DeleteIcon } from "./icons/delete-icon.tsx"; + +export type func_IsAdmin = (pubkey: string) => boolean; interface Props { myPublicKey: PublicKey; messages: ChatMessage[]; emit: emitFunc< - DirectMessagePanelUpdate | SelectConversation | SyncEvent | ViewUserDetail | ReplyToMessage + | DirectMessagePanelUpdate + | SelectConversation + | SyncEvent + | ViewUserDetail + | ReplyToMessage + | DeleteEvent >; getters: { messageGetter: ChatMessagesGetter; getProfileByPublicKey: func_GetProfileByPublicKey; relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; + isAdmin: func_IsAdmin | undefined; }; } @@ -278,16 +287,23 @@ function MessageBoxGroup(props: { messages: ChatMessage[]; myPublicKey: PublicKey; emit: emitFunc< - DirectMessagePanelUpdate | ViewUserDetail | SelectConversation | SyncEvent | ReplyToMessage + | DirectMessagePanelUpdate + | ViewUserDetail + | SelectConversation + | SyncEvent + | ReplyToMessage + | DeleteEvent >; getters: { messageGetter: ChatMessagesGetter; getProfileByPublicKey: func_GetProfileByPublicKey; relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; + isAdmin: func_IsAdmin | undefined; }; }) { const first_message = props.messages[0]; + const { myPublicKey } = props; const rows = []; rows.push( @@ -296,7 +312,12 @@ function MessageBoxGroup(props: { isMobile() ? "select-none" : "" }`} > - {MessageActions(first_message, props.emit)} + {MessageActions({ + isAdmin: props.getters.isAdmin, + myPublicKey, + message: first_message, + emit: props.emit, + })} {renderRelply(first_message.event, props.getters, props.emit)}
- {MessageActions(msg, props.emit)} + {MessageActions({ + isAdmin: props.getters.isAdmin, + myPublicKey, + message: msg, + emit: props.emit, + })} {Time(msg.created_at)}
, -) { +function MessageActions(args: { + myPublicKey: PublicKey; + message: ChatMessage; + emit: emitFunc; + isAdmin: func_IsAdmin | undefined; +}) { + const { myPublicKey, message, emit, isAdmin } = args; return (
+ {(myPublicKey.hex === message.author.hex || (isAdmin && isAdmin(myPublicKey.hex))) && + (message.event.kind === NostrKind.TEXT_NOTE) && + ( + + )} + ) diff --git a/app/database.ts b/app/database.ts index d9e7283..e1fa096 100644 --- a/app/database.ts +++ b/app/database.ts @@ -65,6 +65,7 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover private readonly sourceOfChange = csp.chan<{ event: Parsed_Event; relay?: string }>(buffer_size); private readonly caster = csp.multi<{ event: Parsed_Event; relay?: string }>(this.sourceOfChange); private readonly profiles = new Map(); + private readonly deletionEvents = new Map(); private constructor( private readonly eventsAdapter: EventsAdapter, @@ -125,18 +126,22 @@ 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 e of db.events.values()) { - if (e.kind == NostrKind.META_DATA) { + for (const event of db.events.values()) { + if (event.kind == NostrKind.META_DATA) { // @ts-ignore - const pEvent = parseProfileEvent(e); + const pEvent = parseProfileEvent(event); if (pEvent instanceof Error) { console.error(pEvent); continue; } db.setProfile(pEvent); + } else if (event.kind == NostrKind.DELETE) { + event.parsedTags.e.forEach((event_id) => { + db.deletionEvents.set(event_id, event); + }); } } - + console.log(`Datebase_View:Deletion events size: ${db.deletionEvents.size}`); return db; } @@ -159,6 +164,19 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover } } + isDeleted(id: string, admin?: string) { + const deletionEvent = this.deletionEvents.get(id); + if (deletionEvent == undefined) { + return false; + } + const targetEvent = this.getEventByID(id); + if (targetEvent == undefined) { + return false; + } + return deletionEvent.pubkey == targetEvent.publicKey.hex || + deletionEvent.pubkey == admin; + } + async remove(id: string): Promise { this.removedEvents.add(id); await this.eventMarker.markEvent(id, "removed"); @@ -274,6 +292,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover return pEvent; } this.setProfile(pEvent); + } else if (parsedEvent.kind == NostrKind.DELETE) { + parsedEvent.parsedTags.e.forEach((event_id) => { + this.deletionEvents.set(event_id, parsedEvent); + }); } await this.eventsAdapter.put(event);