Browse Source

follow and mute events, cont.

imwald
Silberengel 1 month ago
parent
commit
242213c9c5
  1. 13
      src/components/NotificationThreadWatchButtons/index.tsx
  2. 1
      src/constants.ts
  3. 3
      src/index.css
  4. 40
      src/lib/notification-thread-watch.ts
  5. 9
      src/lib/personal-list-mutations.ts
  6. 5
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  7. 368
      src/providers/NotificationThreadWatchProvider.tsx

13
src/components/NotificationThreadWatchButtons/index.tsx

@ -6,22 +6,22 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
export default function NotificationThreadWatchButtons({ event }: { event: Event }) { export default function NotificationThreadWatchButtons({ event }: { event: Event }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey } = useNostr() const { pubkey, checkLogin } = useNostr()
const watch = useNotificationThreadWatchOptional() const watch = useNotificationThreadWatchOptional()
const [busy, setBusy] = useState<'follow' | 'mute' | null>(null) const [busy, setBusy] = useState<'follow' | 'mute' | null>(null)
// Show for your own notes too (e.g. notifications feed): you may still want follow/mute on that anchor.
if (!watch || !pubkey) return null if (!watch || !pubkey) return null
if (hexPubkeysEqual(event.pubkey, normalizeHexPubkey(pubkey))) return null
const followed = watch.isFollowedForNotifications(event) const followed = watch.isFollowedForNotifications(event)
const muted = watch.isMutedForNotifications(event) const muted = watch.isMutedForNotifications(event)
const onFollow = async (e: React.MouseEvent) => { const onFollow = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
void checkLogin(async () => {
setBusy('follow') setBusy('follow')
try { try {
if (followed) { if (followed) {
@ -40,10 +40,12 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event
} finally { } finally {
setBusy(null) setBusy(null)
} }
})
} }
const onMute = async (e: React.MouseEvent) => { const onMute = (e: React.MouseEvent) => {
e.stopPropagation() e.stopPropagation()
void checkLogin(async () => {
setBusy('mute') setBusy('mute')
try { try {
if (muted) { if (muted) {
@ -62,6 +64,7 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event
} finally { } finally {
setBusy(null) setBusy(null)
} }
})
} }
return ( return (

1
src/constants.ts

@ -446,7 +446,6 @@ export const SOCIAL_KIND_BLOCKED_RELAY_URLS = [
*/ */
export const E_TAG_FILTER_BLOCKED_RELAY_URLS = [ export const E_TAG_FILTER_BLOCKED_RELAY_URLS = [
'wss://nostr.v0l.io', 'wss://nostr.v0l.io',
'wss://nostr.sovbit.host'
] ]
// Optimized relay list for read operations (includes aggregator) // Optimized relay list for read operations (includes aggregator)

3
src/index.css

@ -210,7 +210,8 @@
--muted-foreground: 140 8% 72%; --muted-foreground: 140 8% 72%;
--accent: 150 14% 18%; --accent: 150 14% 18%;
--accent-foreground: 100 10% 95%; --accent-foreground: 100 10% 95%;
--destructive: 0 62.8% 30.6%; /* Was ~31% L — too dark on popovers/menus; use ~60% L for readable red labels (Report, Mute, …). */
--destructive: 0 72% 60%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 150 12% 22%; --border: 150 12% 22%;
--input: 150 12% 18%; --input: 150 12% 18%;

40
src/lib/notification-thread-watch.ts

@ -1,8 +1,10 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { import {
getParentEventHexId, getParentEventHexId,
getReplaceableCoordinateFromEvent,
getRootEventHexId, getRootEventHexId,
isNip18RepostKind, isNip18RepostKind,
isReplaceableEvent,
isReplyNoteEvent, isReplyNoteEvent,
normalizeReplaceableCoordinateString, normalizeReplaceableCoordinateString,
resolveDeclaredThreadRootEventHex resolveDeclaredThreadRootEventHex
@ -86,39 +88,17 @@ export function threadWatchMatchesRefs(
return false return false
} }
function threadWatchListTagMatchesEvent(tag: string[], event: Event): boolean {
const k = tag[0]
if ((k === 'e' || k === 'E') && tag[1] && /^[0-9a-f]{64}$/i.test(tag[1])) {
const id = tag[1].toLowerCase()
const refs: TThreadWatchListRefs = { eHexLower: new Set([id]), aCoordLower: new Set() }
return threadWatchMatchesRefs(event, refs)
}
if ((k === 'a' || k === 'A') && tag[1]) {
const n = normalizeReplaceableCoordinateString(tag[1])
if (!n) return false
const refs: TThreadWatchListRefs = { eHexLower: new Set(), aCoordLower: new Set([n]) }
return threadWatchMatchesRefs(event, refs)
}
return false
}
/** /**
* Drops every `e` / `a` ref that applies to `event` (same rules as {@link threadWatchMatchesRefs}), * True if the list contains this **exact** event (`e` = {@link Event.id}, or `a` = replaceable coordinate).
* so toggling off works when the list stores a thread root id but the UI row is a reply (or vice versa). * Use for per-note bell UI and for writing list updates. For any reply in this thread, use {@link threadWatchMatchesRefs}.
*/ */
export function listTagsAfterRemovingThreadWatchMatches( export function eventHasExactNotificationThreadWatchRef(event: Event, refs: TThreadWatchListRefs): boolean {
listTags: string[][], if (!refs.eHexLower.size && !refs.aCoordLower.size) return false
event: Event if (isReplaceableEvent(event.kind)) {
): string[][] | null { const n = normalizeReplaceableCoordinateString(getReplaceableCoordinateFromEvent(event))
let changed = false return !!n && refs.aCoordLower.has(n)
const next = listTags.filter((t) => {
if (threadWatchListTagMatchesEvent(t, event)) {
changed = true
return false
} }
return true return refs.eHexLower.has(event.id.toLowerCase())
})
return changed ? next : null
} }
/** Replies, reactions, reposts, zaps-on-note, comments, poll votes, highlights — not plain top-level notes. */ /** Replies, reactions, reposts, zaps-on-note, comments, poll votes, highlights — not plain top-level notes. */

9
src/lib/personal-list-mutations.ts

@ -1,3 +1,4 @@
import { normalizeReplaceableCoordinateString } from '@/lib/event'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
/** Decoded target for one bookmark/pin list entry (NIP-19 nevent/note or naddr). */ /** Decoded target for one bookmark/pin list entry (NIP-19 nevent/note or naddr). */
@ -32,8 +33,12 @@ export function bookmarkListTagsAfterRemovingRef(
): string[][] | null { ): string[][] | null {
if (!ref.eIdLower && !ref.aCoordLower) return null if (!ref.eIdLower && !ref.aCoordLower) return null
const next = tags.filter((tag) => { const next = tags.filter((tag) => {
if (ref.eIdLower && tag[0] === 'e' && tag[1]?.toLowerCase() === ref.eIdLower) return false const k = tag[0]
if (ref.aCoordLower && tag[0] === 'a' && tag[1]?.toLowerCase() === ref.aCoordLower) return false if (ref.eIdLower && (k === 'e' || k === 'E') && tag[1]?.toLowerCase() === ref.eIdLower) return false
if (ref.aCoordLower && (k === 'a' || k === 'A') && tag[1]) {
const n = normalizeReplaceableCoordinateString(tag[1])
if (n === ref.aCoordLower) return false
}
return true return true
}) })
return next.length === tags.length ? null : next return next.length === tags.length ? null : next

5
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -579,6 +579,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
const followRefs = parseThreadWatchListRefs(notificationEventsIFollowListEvent ?? null) const followRefs = parseThreadWatchListRefs(notificationEventsIFollowListEvent ?? null)
const mutedRefs = parseThreadWatchListRefs(notificationEventsIMutedListEvent ?? null) const mutedRefs = parseThreadWatchListRefs(notificationEventsIMutedListEvent ?? null)
// Never list your own authored events in this account's notifications (`#p` REQ still returns self-replies, self-`#p`, etc.).
if (hexPubkeysEqual(evt.pubkey, pk)) return true
if ( if (
threadWatchMatchesRefs(evt, mutedRefs) && threadWatchMatchesRefs(evt, mutedRefs) &&
isNotificationThreadInteractionEvent(evt) isNotificationThreadInteractionEvent(evt)
@ -588,8 +591,6 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
if (isUserInEventMentions(evt, pk)) return false if (isUserInEventMentions(evt, pk)) return false
if (hexPubkeysEqual(evt.pubkey, pk)) return false
if ( if (
threadWatchMatchesRefs(evt, followRefs) && threadWatchMatchesRefs(evt, followRefs) &&
isNotificationThreadInteractionEvent(evt) isNotificationThreadInteractionEvent(evt)

368
src/providers/NotificationThreadWatchProvider.tsx

@ -8,15 +8,23 @@ import {
} from '@/lib/personal-list-mutations' } from '@/lib/personal-list-mutations'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import { import {
listTagsAfterRemovingThreadWatchMatches, eventHasExactNotificationThreadWatchRef,
parseThreadWatchListRefs, parseThreadWatchListRefs
threadWatchMatchesRefs
} from '@/lib/notification-thread-watch' } from '@/lib/notification-thread-watch'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import type { Event } from 'nostr-tools' import type { Event } from 'nostr-tools'
import { useCallback, useContext, useEffect, useMemo, useState, createContext, type ReactNode } from 'react' import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
createContext,
type ReactNode
} from 'react'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -79,10 +87,15 @@ function mergeTagsPreservingMeta(baseTags: string[][], refTags: string[][]): str
} }
export function NotificationThreadWatchProvider({ children }: { children: ReactNode }) { export function NotificationThreadWatchProvider({ children }: { children: ReactNode }) {
const { pubkey: accountPubkey, publish } = useNostr() const { pubkey: accountPubkey, publish, signEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays() const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [eventsIFollowListEvent, setEventsIFollowListEvent] = useState<Event | null>(null) const [eventsIFollowListEvent, setEventsIFollowListEvent] = useState<Event | null>(null)
const [eventsIMutedListEvent, setEventsIMutedListEvent] = useState<Event | null>(null) const [eventsIMutedListEvent, setEventsIMutedListEvent] = useState<Event | null>(null)
/** Same as state, updated during render so async handlers never read a stale list before effects run. */
const eventsIFollowListEventRef = useRef<Event | null>(null)
const eventsIMutedListEventRef = useRef<Event | null>(null)
eventsIFollowListEventRef.current = eventsIFollowListEvent
eventsIMutedListEventRef.current = eventsIMutedListEvent
const buildComprehensiveRelayList = useCallback(async () => { const buildComprehensiveRelayList = useCallback(async () => {
if (!accountPubkey) return [] as string[] if (!accountPubkey) return [] as string[]
@ -134,14 +147,10 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
} }
const f = pick(remoteFollow, idbFollow ?? undefined) const f = pick(remoteFollow, idbFollow ?? undefined)
const m = pick(remoteMuted, idbMuted ?? undefined) const m = pick(remoteMuted, idbMuted ?? undefined)
if (f) { if (f) await indexedDb.putReplaceableEvent(f)
await indexedDb.putReplaceableEvent(f) if (m) await indexedDb.putReplaceableEvent(m)
setEventsIFollowListEvent(f) setEventsIFollowListEvent(f ?? null)
} setEventsIMutedListEvent(m ?? null)
if (m) {
await indexedDb.putReplaceableEvent(m)
setEventsIMutedListEvent(m)
}
}, [accountPubkey, buildComprehensiveRelayList, hydrateFromStorage]) }, [accountPubkey, buildComprehensiveRelayList, hydrateFromStorage])
useEffect(() => { useEffect(() => {
@ -174,17 +183,17 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
) )
const isFollowedForNotifications = useCallback( const isFollowedForNotifications = useCallback(
(event: Event) => threadWatchMatchesRefs(event, followRefs), (event: Event) => eventHasExactNotificationThreadWatchRef(event, followRefs),
[followRefs] [followRefs]
) )
const isMutedForNotifications = useCallback( const isMutedForNotifications = useCallback(
(event: Event) => threadWatchMatchesRefs(event, mutedRefs), (event: Event) => eventHasExactNotificationThreadWatchRef(event, mutedRefs),
[mutedRefs] [mutedRefs]
) )
const publishList = useCallback( const relayPublishList = useCallback(
async (kind: number, nextTags: string[][], content: string) => { async (kind: number, nextTags: string[][], content: string) => {
if (!accountPubkey) return if (!accountPubkey) throw new Error('Not logged in')
const comprehensiveRelays = await buildComprehensiveRelayList() const comprehensiveRelays = await buildComprehensiveRelayList()
const draft = createReplaceablePersonalListDraftEvent(kind, nextTags, content) const draft = createReplaceablePersonalListDraftEvent(kind, nextTags, content)
const ev = await publish(draft, { specifiedRelayUrls: comprehensiveRelays }) const ev = await publish(draft, { specifiedRelayUrls: comprehensiveRelays })
@ -194,213 +203,266 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
} else { } else {
setEventsIMutedListEvent(stored) setEventsIMutedListEvent(stored)
} }
return stored
}, },
[accountPubkey, buildComprehensiveRelayList, publish] [accountPubkey, buildComprehensiveRelayList, publish]
) )
const signApplyListLocal = useCallback(
async (kind: number, nextTags: string[][], content: string): Promise<Event> => {
const draft = createReplaceablePersonalListDraftEvent(kind, nextTags, content)
const ev = await signEvent(draft)
if (kind === ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST) {
setEventsIFollowListEvent(ev)
} else {
setEventsIMutedListEvent(ev)
}
void indexedDb.putReplaceableEvent(ev).catch((err) =>
logger.warn('NotificationThreadWatchProvider: optimistic IndexedDB write failed', { err })
)
return ev
},
[signEvent]
)
const restoreListSnapshots = useCallback(async (snapFollow: Event | null, snapMuted: Event | null) => {
setEventsIFollowListEvent(snapFollow)
setEventsIMutedListEvent(snapMuted)
try {
await Promise.all([
snapFollow ? indexedDb.putReplaceableEvent(snapFollow) : Promise.resolve(),
snapMuted ? indexedDb.putReplaceableEvent(snapMuted) : Promise.resolve()
])
} catch (err) {
logger.warn('NotificationThreadWatchProvider: rollback IndexedDB write failed', { err })
}
}, [])
const followThreadForNotifications = useCallback( const followThreadForNotifications = useCallback(
async (event: Event) => { async (event: Event) => {
if (!accountPubkey) return if (!accountPubkey) return
const comprehensiveRelays = await buildComprehensiveRelayList() const prevF = eventsIFollowListEventRef.current
const prevM = eventsIMutedListEventRef.current
const snapF = prevF
const snapM = prevM
const refTag = isReplaceableEvent(event.kind) ? buildATag(event) : buildETag(event.id, event.pubkey) const refTag = isReplaceableEvent(event.kind) ? buildATag(event) : buildETag(event.id, event.pubkey)
let followEv = const ref = refKeyForEvent(event)
(await fetchLatestReplaceableListEvent(
accountPubkey, const mutedStripped = prevM ? listTagsWithoutRef(prevM.tags, ref) : null
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
comprehensiveRelays const curFollowRefs = parseThreadWatchListRefs(prevF)
)) ?? null if (eventHasExactNotificationThreadWatchRef(event, curFollowRefs)) {
if (!followEv) { if (prevF) {
followEv = try {
(await indexedDb.getReplaceableEvent( await indexedDb.putReplaceableEvent(prevF)
accountPubkey.trim().toLowerCase(), } catch {
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST /* ignore */
)) ?? null
} }
let mutedEv = setEventsIFollowListEvent(prevF)
(await fetchLatestReplaceableListEvent( }
accountPubkey, return
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!mutedEv) {
mutedEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
)) ?? null
} }
const mutedStripped = mutedEv ? listTagsAfterRemovingThreadWatchMatches(mutedEv.tags, event) : null const nextFollowTags = mergeTagsPreservingMeta(prevF?.tags ?? [], [refTag])
if (mutedStripped) { const followContent = prevF?.content ?? ''
await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, mutedStripped, mutedEv.content)
try {
if (mutedStripped && prevM) {
await signApplyListLocal(
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
mutedStripped,
prevM.content
)
} }
await signApplyListLocal(
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
nextFollowTags,
followContent
)
const curTags = followEv?.tags ?? [] if (mutedStripped && prevM) {
const curFollowRefs = parseThreadWatchListRefs(followEv) await relayPublishList(
if (threadWatchMatchesRefs(event, curFollowRefs)) { ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
return mutedStripped,
prevM.content
)
} }
const next = mergeTagsPreservingMeta(curTags, [refTag]) await relayPublishList(
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv?.content ?? '') ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
nextFollowTags,
followContent
)
logger.component('NotificationThreadWatchProvider', 'follow thread for notifications', { logger.component('NotificationThreadWatchProvider', 'follow thread for notifications', {
kind: event.kind kind: event.kind
}) })
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const muteThreadForNotifications = useCallback( const muteThreadForNotifications = useCallback(
async (event: Event) => { async (event: Event) => {
if (!accountPubkey) return if (!accountPubkey) return
const comprehensiveRelays = await buildComprehensiveRelayList() const prevF = eventsIFollowListEventRef.current
const prevM = eventsIMutedListEventRef.current
const snapF = prevF
const snapM = prevM
const refTag = isReplaceableEvent(event.kind) ? buildATag(event) : buildETag(event.id, event.pubkey) const refTag = isReplaceableEvent(event.kind) ? buildATag(event) : buildETag(event.id, event.pubkey)
let mutedEv = const ref = refKeyForEvent(event)
(await fetchLatestReplaceableListEvent(
accountPubkey, const followStripped = prevF ? listTagsWithoutRef(prevF.tags, ref) : null
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
comprehensiveRelays const curMutedRefs = parseThreadWatchListRefs(prevM)
)) ?? null if (eventHasExactNotificationThreadWatchRef(event, curMutedRefs)) {
if (!mutedEv) { if (prevM) {
mutedEv = try {
(await indexedDb.getReplaceableEvent( await indexedDb.putReplaceableEvent(prevM)
accountPubkey.trim().toLowerCase(), } catch {
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST /* ignore */
)) ?? null
} }
let followEv = setEventsIMutedListEvent(prevM)
(await fetchLatestReplaceableListEvent(
accountPubkey,
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!followEv) {
followEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST
)) ?? null
} }
return
}
const nextMutedTags = mergeTagsPreservingMeta(prevM?.tags ?? [], [refTag])
const mutedContent = prevM?.content ?? ''
const followStripped = followEv ? listTagsAfterRemovingThreadWatchMatches(followEv.tags, event) : null try {
if (followStripped) { if (followStripped && prevF) {
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, followStripped, followEv.content) await signApplyListLocal(
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
followStripped,
prevF.content
)
} }
await signApplyListLocal(
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
nextMutedTags,
mutedContent
)
const curTags = mutedEv?.tags ?? [] if (followStripped && prevF) {
const curMutedRefs = parseThreadWatchListRefs(mutedEv) await relayPublishList(
if (threadWatchMatchesRefs(event, curMutedRefs)) { ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
return followStripped,
prevF.content
)
}
await relayPublishList(
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
nextMutedTags,
mutedContent
)
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
} }
const next = mergeTagsPreservingMeta(curTags, [refTag])
await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv?.content ?? '')
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const unfollowThreadForNotifications = useCallback( const unfollowThreadForNotifications = useCallback(
async (event: Event): Promise<boolean> => { async (event: Event): Promise<boolean> => {
if (!accountPubkey) return false if (!accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList() const pk = accountPubkey.trim().toLowerCase()
let followEv = let prevF =
(await fetchLatestReplaceableListEvent( eventsIFollowListEventRef.current ??
accountPubkey, (await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST))
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, if (!prevF) return false
comprehensiveRelays const next = listTagsWithoutRef(prevF.tags, refKeyForEvent(event))
)) ?? null
if (!followEv) {
followEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST
)) ?? null
}
if (!followEv) return false
const next = listTagsAfterRemovingThreadWatchMatches(followEv.tags, event)
if (!next) return false if (!next) return false
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content) const snapF = prevF
const snapM = eventsIMutedListEventRef.current
try {
await signApplyListLocal(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, prevF.content)
await relayPublishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, prevF.content)
return true return true
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const unmuteThreadForNotifications = useCallback( const unmuteThreadForNotifications = useCallback(
async (event: Event): Promise<boolean> => { async (event: Event): Promise<boolean> => {
if (!accountPubkey) return false if (!accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList() const pk = accountPubkey.trim().toLowerCase()
let mutedEv = let prevM =
(await fetchLatestReplaceableListEvent( eventsIMutedListEventRef.current ??
accountPubkey, (await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST))
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, if (!prevM) return false
comprehensiveRelays const next = listTagsWithoutRef(prevM.tags, refKeyForEvent(event))
)) ?? null
if (!mutedEv) {
mutedEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
)) ?? null
}
if (!mutedEv) return false
const next = listTagsAfterRemovingThreadWatchMatches(mutedEv.tags, event)
if (!next) return false if (!next) return false
await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content) const snapF = eventsIFollowListEventRef.current
const snapM = prevM
try {
await signApplyListLocal(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, prevM.content)
await relayPublishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, prevM.content)
return true return true
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const removeFollowRefByBech32 = useCallback( const removeFollowRefByBech32 = useCallback(
async (bech32Id: string): Promise<boolean> => { async (bech32Id: string): Promise<boolean> => {
const ref = decodePersonalListBech32Ref(bech32Id) const ref = decodePersonalListBech32Ref(bech32Id)
if (!ref || !accountPubkey) return false if (!ref || !accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList() const pk = accountPubkey.trim().toLowerCase()
let followEv = let followEv =
(await fetchLatestReplaceableListEvent( eventsIFollowListEventRef.current ??
accountPubkey, (await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST))
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!followEv) {
followEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST
)) ?? null
}
if (!followEv) return false if (!followEv) return false
const next = listTagsWithoutRef(followEv.tags, ref) const next = listTagsWithoutRef(followEv.tags, ref)
if (!next) return false if (!next) return false
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content) const snapF = followEv
const snapM = eventsIMutedListEventRef.current
try {
await signApplyListLocal(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content)
await relayPublishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content)
return true return true
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const removeMuteRefByBech32 = useCallback( const removeMuteRefByBech32 = useCallback(
async (bech32Id: string): Promise<boolean> => { async (bech32Id: string): Promise<boolean> => {
const ref = decodePersonalListBech32Ref(bech32Id) const ref = decodePersonalListBech32Ref(bech32Id)
if (!ref || !accountPubkey) return false if (!ref || !accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList() const pk = accountPubkey.trim().toLowerCase()
let mutedEv = let mutedEv =
(await fetchLatestReplaceableListEvent( eventsIMutedListEventRef.current ??
accountPubkey, (await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST))
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!mutedEv) {
mutedEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
)) ?? null
}
if (!mutedEv) return false if (!mutedEv) return false
const next = listTagsWithoutRef(mutedEv.tags, ref) const next = listTagsWithoutRef(mutedEv.tags, ref)
if (!next) return false if (!next) return false
await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content) const snapF = eventsIFollowListEventRef.current
const snapM = mutedEv
try {
await signApplyListLocal(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content)
await relayPublishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content)
return true return true
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
}, },
[accountPubkey, buildComprehensiveRelayList, publishList] [accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
) )
const value = useMemo( const value = useMemo(

Loading…
Cancel
Save