feat(archive): support un-archive toggle; add ArchiveController mark/unmark; prep NIP-09 deletion hook

This commit is contained in:
Gigi
2025-10-20 11:21:59 +02:00
parent 8cde36c08c
commit 470f4fb34e
3 changed files with 65 additions and 1 deletions

View File

@@ -599,7 +599,32 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
}, [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<ContentPanelProps> = ({
// 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<ContentPanelProps> = ({
// 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) {

View File

@@ -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)
}

View File

@@ -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<NostrEvent> {
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