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' @@ -6,22 +6,22 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useNostr } from '@/providers/NostrProvider'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
export default function NotificationThreadWatchButtons({ event }: { event: Event }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { pubkey, checkLogin } = useNostr()
const watch = useNotificationThreadWatchOptional()
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 (hexPubkeysEqual(event.pubkey, normalizeHexPubkey(pubkey))) return null
const followed = watch.isFollowedForNotifications(event)
const muted = watch.isMutedForNotifications(event)
const onFollow = async (e: React.MouseEvent) => {
const onFollow = (e: React.MouseEvent) => {
e.stopPropagation()
void checkLogin(async () => {
setBusy('follow')
try {
if (followed) {
@ -40,10 +40,12 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event @@ -40,10 +40,12 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event
} finally {
setBusy(null)
}
})
}
const onMute = async (e: React.MouseEvent) => {
const onMute = (e: React.MouseEvent) => {
e.stopPropagation()
void checkLogin(async () => {
setBusy('mute')
try {
if (muted) {
@ -62,6 +64,7 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event @@ -62,6 +64,7 @@ export default function NotificationThreadWatchButtons({ event }: { event: Event
} finally {
setBusy(null)
}
})
}
return (

1
src/constants.ts

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

3
src/index.css

@ -210,7 +210,8 @@ @@ -210,7 +210,8 @@
--muted-foreground: 140 8% 72%;
--accent: 150 14% 18%;
--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%;
--border: 150 12% 22%;
--input: 150 12% 18%;

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

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
import { ExtendedKind } from '@/constants'
import {
getParentEventHexId,
getReplaceableCoordinateFromEvent,
getRootEventHexId,
isNip18RepostKind,
isReplaceableEvent,
isReplyNoteEvent,
normalizeReplaceableCoordinateString,
resolveDeclaredThreadRootEventHex
@ -86,39 +88,17 @@ export function threadWatchMatchesRefs( @@ -86,39 +88,17 @@ export function threadWatchMatchesRefs(
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}),
* so toggling off works when the list stores a thread root id but the UI row is a reply (or vice versa).
* True if the list contains this **exact** event (`e` = {@link Event.id}, or `a` = replaceable coordinate).
* Use for per-note bell UI and for writing list updates. For any reply in this thread, use {@link threadWatchMatchesRefs}.
*/
export function listTagsAfterRemovingThreadWatchMatches(
listTags: string[][],
event: Event
): string[][] | null {
let changed = false
const next = listTags.filter((t) => {
if (threadWatchListTagMatchesEvent(t, event)) {
changed = true
return false
export function eventHasExactNotificationThreadWatchRef(event: Event, refs: TThreadWatchListRefs): boolean {
if (!refs.eHexLower.size && !refs.aCoordLower.size) return false
if (isReplaceableEvent(event.kind)) {
const n = normalizeReplaceableCoordinateString(getReplaceableCoordinateFromEvent(event))
return !!n && refs.aCoordLower.has(n)
}
return true
})
return changed ? next : null
return refs.eHexLower.has(event.id.toLowerCase())
}
/** 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 @@ @@ -1,3 +1,4 @@
import { normalizeReplaceableCoordinateString } from '@/lib/event'
import { nip19 } from 'nostr-tools'
/** Decoded target for one bookmark/pin list entry (NIP-19 nevent/note or naddr). */
@ -32,8 +33,12 @@ export function bookmarkListTagsAfterRemovingRef( @@ -32,8 +33,12 @@ export function bookmarkListTagsAfterRemovingRef(
): string[][] | null {
if (!ref.eIdLower && !ref.aCoordLower) return null
const next = tags.filter((tag) => {
if (ref.eIdLower && tag[0] === 'e' && tag[1]?.toLowerCase() === ref.eIdLower) return false
if (ref.aCoordLower && tag[0] === 'a' && tag[1]?.toLowerCase() === ref.aCoordLower) return false
const k = tag[0]
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 next.length === tags.length ? null : next

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

@ -579,6 +579,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -579,6 +579,9 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
const followRefs = parseThreadWatchListRefs(notificationEventsIFollowListEvent ?? 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 (
threadWatchMatchesRefs(evt, mutedRefs) &&
isNotificationThreadInteractionEvent(evt)
@ -588,8 +591,6 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) { @@ -588,8 +591,6 @@ export function useSpellsPageFeed(a: UseSpellsPageFeedArgs) {
if (isUserInEventMentions(evt, pk)) return false
if (hexPubkeysEqual(evt.pubkey, pk)) return false
if (
threadWatchMatchesRefs(evt, followRefs) &&
isNotificationThreadInteractionEvent(evt)

368
src/providers/NotificationThreadWatchProvider.tsx

@ -8,15 +8,23 @@ import { @@ -8,15 +8,23 @@ import {
} from '@/lib/personal-list-mutations'
import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import {
listTagsAfterRemovingThreadWatchMatches,
parseThreadWatchListRefs,
threadWatchMatchesRefs
eventHasExactNotificationThreadWatchRef,
parseThreadWatchListRefs
} from '@/lib/notification-thread-watch'
import logger from '@/lib/logger'
import { ExtendedKind } from '@/constants'
import indexedDb from '@/services/indexed-db.service'
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 { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -79,10 +87,15 @@ function mergeTagsPreservingMeta(baseTags: string[][], refTags: string[][]): str @@ -79,10 +87,15 @@ function mergeTagsPreservingMeta(baseTags: string[][], refTags: string[][]): str
}
export function NotificationThreadWatchProvider({ children }: { children: ReactNode }) {
const { pubkey: accountPubkey, publish } = useNostr()
const { pubkey: accountPubkey, publish, signEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [eventsIFollowListEvent, setEventsIFollowListEvent] = 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 () => {
if (!accountPubkey) return [] as string[]
@ -134,14 +147,10 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN @@ -134,14 +147,10 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
}
const f = pick(remoteFollow, idbFollow ?? undefined)
const m = pick(remoteMuted, idbMuted ?? undefined)
if (f) {
await indexedDb.putReplaceableEvent(f)
setEventsIFollowListEvent(f)
}
if (m) {
await indexedDb.putReplaceableEvent(m)
setEventsIMutedListEvent(m)
}
if (f) await indexedDb.putReplaceableEvent(f)
if (m) await indexedDb.putReplaceableEvent(m)
setEventsIFollowListEvent(f ?? null)
setEventsIMutedListEvent(m ?? null)
}, [accountPubkey, buildComprehensiveRelayList, hydrateFromStorage])
useEffect(() => {
@ -174,17 +183,17 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN @@ -174,17 +183,17 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
)
const isFollowedForNotifications = useCallback(
(event: Event) => threadWatchMatchesRefs(event, followRefs),
(event: Event) => eventHasExactNotificationThreadWatchRef(event, followRefs),
[followRefs]
)
const isMutedForNotifications = useCallback(
(event: Event) => threadWatchMatchesRefs(event, mutedRefs),
(event: Event) => eventHasExactNotificationThreadWatchRef(event, mutedRefs),
[mutedRefs]
)
const publishList = useCallback(
const relayPublishList = useCallback(
async (kind: number, nextTags: string[][], content: string) => {
if (!accountPubkey) return
if (!accountPubkey) throw new Error('Not logged in')
const comprehensiveRelays = await buildComprehensiveRelayList()
const draft = createReplaceablePersonalListDraftEvent(kind, nextTags, content)
const ev = await publish(draft, { specifiedRelayUrls: comprehensiveRelays })
@ -194,213 +203,266 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN @@ -194,213 +203,266 @@ export function NotificationThreadWatchProvider({ children }: { children: ReactN
} else {
setEventsIMutedListEvent(stored)
}
return stored
},
[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(
async (event: Event) => {
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)
let followEv =
(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
const ref = refKeyForEvent(event)
const mutedStripped = prevM ? listTagsWithoutRef(prevM.tags, ref) : null
const curFollowRefs = parseThreadWatchListRefs(prevF)
if (eventHasExactNotificationThreadWatchRef(event, curFollowRefs)) {
if (prevF) {
try {
await indexedDb.putReplaceableEvent(prevF)
} catch {
/* ignore */
}
let mutedEv =
(await fetchLatestReplaceableListEvent(
accountPubkey,
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!mutedEv) {
mutedEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
)) ?? null
setEventsIFollowListEvent(prevF)
}
return
}
const mutedStripped = mutedEv ? listTagsAfterRemovingThreadWatchMatches(mutedEv.tags, event) : null
if (mutedStripped) {
await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, mutedStripped, mutedEv.content)
const nextFollowTags = mergeTagsPreservingMeta(prevF?.tags ?? [], [refTag])
const followContent = prevF?.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 ?? []
const curFollowRefs = parseThreadWatchListRefs(followEv)
if (threadWatchMatchesRefs(event, curFollowRefs)) {
return
if (mutedStripped && prevM) {
await relayPublishList(
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
mutedStripped,
prevM.content
)
}
const next = mergeTagsPreservingMeta(curTags, [refTag])
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv?.content ?? '')
await relayPublishList(
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
nextFollowTags,
followContent
)
logger.component('NotificationThreadWatchProvider', 'follow thread for notifications', {
kind: event.kind
})
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
},
[accountPubkey, buildComprehensiveRelayList, publishList]
[accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
)
const muteThreadForNotifications = useCallback(
async (event: Event) => {
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)
let mutedEv =
(await fetchLatestReplaceableListEvent(
accountPubkey,
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 ref = refKeyForEvent(event)
const followStripped = prevF ? listTagsWithoutRef(prevF.tags, ref) : null
const curMutedRefs = parseThreadWatchListRefs(prevM)
if (eventHasExactNotificationThreadWatchRef(event, curMutedRefs)) {
if (prevM) {
try {
await indexedDb.putReplaceableEvent(prevM)
} catch {
/* ignore */
}
let followEv =
(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
setEventsIMutedListEvent(prevM)
}
return
}
const nextMutedTags = mergeTagsPreservingMeta(prevM?.tags ?? [], [refTag])
const mutedContent = prevM?.content ?? ''
const followStripped = followEv ? listTagsAfterRemovingThreadWatchMatches(followEv.tags, event) : null
if (followStripped) {
await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, followStripped, followEv.content)
try {
if (followStripped && prevF) {
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 ?? []
const curMutedRefs = parseThreadWatchListRefs(mutedEv)
if (threadWatchMatchesRefs(event, curMutedRefs)) {
return
if (followStripped && prevF) {
await relayPublishList(
ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST,
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(
async (event: Event): Promise<boolean> => {
if (!accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList()
let followEv =
(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
}
if (!followEv) return false
const next = listTagsAfterRemovingThreadWatchMatches(followEv.tags, event)
const pk = accountPubkey.trim().toLowerCase()
let prevF =
eventsIFollowListEventRef.current ??
(await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST))
if (!prevF) return false
const next = listTagsWithoutRef(prevF.tags, refKeyForEvent(event))
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
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
},
[accountPubkey, buildComprehensiveRelayList, publishList]
[accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
)
const unmuteThreadForNotifications = useCallback(
async (event: Event): Promise<boolean> => {
if (!accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList()
let mutedEv =
(await fetchLatestReplaceableListEvent(
accountPubkey,
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
const next = listTagsAfterRemovingThreadWatchMatches(mutedEv.tags, event)
const pk = accountPubkey.trim().toLowerCase()
let prevM =
eventsIMutedListEventRef.current ??
(await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST))
if (!prevM) return false
const next = listTagsWithoutRef(prevM.tags, refKeyForEvent(event))
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
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
},
[accountPubkey, buildComprehensiveRelayList, publishList]
[accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
)
const removeFollowRefByBech32 = useCallback(
async (bech32Id: string): Promise<boolean> => {
const ref = decodePersonalListBech32Ref(bech32Id)
if (!ref || !accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList()
const pk = accountPubkey.trim().toLowerCase()
let followEv =
(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
}
eventsIFollowListEventRef.current ??
(await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST))
if (!followEv) return false
const next = listTagsWithoutRef(followEv.tags, ref)
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
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
},
[accountPubkey, buildComprehensiveRelayList, publishList]
[accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
)
const removeMuteRefByBech32 = useCallback(
async (bech32Id: string): Promise<boolean> => {
const ref = decodePersonalListBech32Ref(bech32Id)
if (!ref || !accountPubkey) return false
const comprehensiveRelays = await buildComprehensiveRelayList()
const pk = accountPubkey.trim().toLowerCase()
let mutedEv =
(await fetchLatestReplaceableListEvent(
accountPubkey,
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST,
comprehensiveRelays
)) ?? null
if (!mutedEv) {
mutedEv =
(await indexedDb.getReplaceableEvent(
accountPubkey.trim().toLowerCase(),
ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
)) ?? null
}
eventsIMutedListEventRef.current ??
(await indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST))
if (!mutedEv) return false
const next = listTagsWithoutRef(mutedEv.tags, ref)
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
} catch (e) {
await restoreListSnapshots(snapF, snapM)
throw e
}
},
[accountPubkey, buildComprehensiveRelayList, publishList]
[accountPubkey, relayPublishList, restoreListSnapshots, signApplyListLocal]
)
const value = useMemo(

Loading…
Cancel
Save