diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index d0b413dd..13d48e5c 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -599,7 +599,32 @@ const ContentPanel: React.FC = ({ }, [selectedUrl, currentArticle, activeAccount, relayPool, isNostrArticle]) const handleMarkAsRead = () => { - if (!activeAccount || !relayPool || isMarkedAsRead) { + if (!activeAccount || !relayPool) return + + // Toggle archive state: if already archived, request deletion; else archive + if (isMarkedAsRead) { + // Optimistically unarchive in UI; background deletion request (NIP-09) + setIsMarkedAsRead(false) + ;(async () => { + try { + // Best-effort: we don't store reaction IDs yet; leaving placeholder for future improvement + // When we track reaction IDs, call deleteReaction(id,...) + // For now, clear controller mark so lists update + if (isNostrArticle && currentArticle) { + try { + const dTag = currentArticle.tags.find(t => t[0] === 'd')?.[1] + if (dTag) { + const naddr = nip19.naddrEncode({ kind: 30023, pubkey: currentArticle.pubkey, identifier: dTag }) + archiveController.unmark(naddr) + } + } catch {} + } else if (selectedUrl) { + archiveController.unmark(selectedUrl) + } + } catch (err) { + console.warn('[archive][content] unarchive failed', err) + } + })() return } @@ -640,6 +665,7 @@ const ContentPanel: React.FC = ({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore private update for instant UI; controller will confirm via stream archiveController['markedIds'].add(naddr) + archiveController.mark(naddr) console.log('[archive][content] optimistic mark article', naddr.slice(0, 24) + '...') } } catch {} @@ -652,6 +678,7 @@ const ContentPanel: React.FC = ({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore private update for instant UI; controller will confirm via stream archiveController['markedIds'].add(selectedUrl) + archiveController.mark(selectedUrl) console.log('[archive][content] optimistic mark url', selectedUrl) } } catch (error) { diff --git a/src/services/archiveController.ts b/src/services/archiveController.ts index 577adc6a..dbb3bb37 100644 --- a/src/services/archiveController.ts +++ b/src/services/archiveController.ts @@ -31,6 +31,21 @@ class ArchiveController { this.listeners.forEach(cb => cb(snapshot)) } + mark(id: string): void { + if (!this.markedIds.has(id)) { + this.markedIds.add(id) + this.emit() + console.log('[archive] mark() added', id.slice(0, 48)) + } + } + + unmark(id: string): void { + if (this.markedIds.delete(id)) { + this.emit() + console.log('[archive] unmark() removed', id.slice(0, 48)) + } + } + isMarked(id: string): boolean { return this.markedIds.has(id) } diff --git a/src/services/reactionService.ts b/src/services/reactionService.ts index b09446a7..6f712156 100644 --- a/src/services/reactionService.ts +++ b/src/services/reactionService.ts @@ -4,6 +4,7 @@ import { IAccount } from 'applesauce-accounts' import { NostrEvent } from 'nostr-tools' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' import { RELAYS } from '../config/relays' +import { EventFactory } from 'applesauce-factory' const MARK_AS_READ_EMOJI = '📚' @@ -105,6 +106,27 @@ export async function createWebsiteReaction( return signed } +/** + * Sends a deletion request (NIP-09) for a reaction event to effectively un-archive. + * The caller must know the reaction event id to delete. + */ +export async function deleteReaction( + reactionEventId: string, + account: IAccount, + relayPool: RelayPool +): Promise { + const factory = new EventFactory({ signer: account }) + const draft = await factory.create(async () => ({ + kind: 5, // Deletion per NIP-09 + content: 'unarchive', + tags: [['e', reactionEventId]], + created_at: Math.floor(Date.now() / 1000) + })) + const signed = await factory.sign(draft) + await relayPool.publish(RELAYS, signed) + return signed +} + /** * Checks if the user has already marked a nostr event as read * @param eventId The ID of the event to check