@@ -92,6 +101,30 @@ const PersonalListsSettingsPage = forwardRef(
navigateToPinList(toPinsList())}>
diff --git a/src/providers/NotificationThreadWatchProvider.tsx b/src/providers/NotificationThreadWatchProvider.tsx
new file mode 100644
index 00000000..66290d39
--- /dev/null
+++ b/src/providers/NotificationThreadWatchProvider.tsx
@@ -0,0 +1,454 @@
+import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls'
+import { buildATag, buildETag, createReplaceablePersonalListDraftEvent } from '@/lib/draft-event'
+import { getReplaceableCoordinateFromEvent, isReplaceableEvent, normalizeReplaceableCoordinateString } from '@/lib/event'
+import {
+ bookmarkListTagsAfterRemovingRef,
+ decodePersonalListBech32Ref,
+ type TPersonalListBech32Ref
+} from '@/lib/personal-list-mutations'
+import { fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
+import {
+ listTagsAfterRemovingThreadWatchMatches,
+ parseThreadWatchListRefs,
+ threadWatchMatchesRefs
+} 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 { useNostr } from '@/providers/NostrProvider'
+import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
+
+export type TNotificationThreadWatchContext = {
+ eventsIFollowListEvent: Event | null
+ eventsIMutedListEvent: Event | null
+ followRefs: ReturnType
+ mutedRefs: ReturnType
+ isFollowedForNotifications: (event: Event) => boolean
+ isMutedForNotifications: (event: Event) => boolean
+ followThreadForNotifications: (event: Event) => Promise
+ muteThreadForNotifications: (event: Event) => Promise
+ unfollowThreadForNotifications: (event: Event) => Promise
+ unmuteThreadForNotifications: (event: Event) => Promise
+ /** Refetch both lists from relays + IDB and update local state (e.g. settings list editor). */
+ refreshNotificationThreadListsFromRelays: () => Promise
+ removeFollowRefByBech32: (bech32Id: string) => Promise
+ removeMuteRefByBech32: (bech32Id: string) => Promise
+}
+
+const NotificationThreadWatchContext = createContext(undefined)
+
+function refKeyForEvent(event: Event): TPersonalListBech32Ref {
+ if (isReplaceableEvent(event.kind)) {
+ const n = normalizeReplaceableCoordinateString(getReplaceableCoordinateFromEvent(event))
+ return { aCoordLower: n }
+ }
+ return { eIdLower: event.id.toLowerCase() }
+}
+
+function listTagsWithoutRef(tags: string[][], ref: TPersonalListBech32Ref): string[][] | null {
+ return bookmarkListTagsAfterRemovingRef(tags, ref)
+}
+
+function mergeTagsPreservingMeta(baseTags: string[][], refTags: string[][]): string[][] {
+ const meta = baseTags.filter(
+ (t) => t[0] === 'title' || t[0] === 'image' || t[0] === 'description' || t[0] === 'd'
+ )
+ const seenE = new Set()
+ const seenA = new Set()
+ const refs: string[][] = []
+ const pushRef = (t: string[]) => {
+ if (t[0] === 'e' || t[0] === 'E') {
+ const id = t[1]?.toLowerCase()
+ if (id && !seenE.has(id)) {
+ seenE.add(id)
+ refs.push(t)
+ }
+ } else if (t[0] === 'a' || t[0] === 'A') {
+ const n = normalizeReplaceableCoordinateString(t[1] ?? '')
+ if (n && !seenA.has(n)) {
+ seenA.add(n)
+ refs.push([t[0], n, ...t.slice(2)])
+ }
+ }
+ }
+ for (const t of baseTags) pushRef(t)
+ for (const t of refTags) pushRef(t)
+ return [...meta, ...refs]
+}
+
+export function NotificationThreadWatchProvider({ children }: { children: ReactNode }) {
+ const { pubkey: accountPubkey, publish } = useNostr()
+ const { favoriteRelays, blockedRelays } = useFavoriteRelays()
+ const [eventsIFollowListEvent, setEventsIFollowListEvent] = useState(null)
+ const [eventsIMutedListEvent, setEventsIMutedListEvent] = useState(null)
+
+ const buildComprehensiveRelayList = useCallback(async () => {
+ if (!accountPubkey) return [] as string[]
+ return buildAccountListRelayUrlsForMerge({
+ accountPubkey,
+ favoriteRelays: favoriteRelays ?? [],
+ blockedRelays
+ })
+ }, [accountPubkey, favoriteRelays, blockedRelays])
+
+ const hydrateFromStorage = useCallback(async () => {
+ if (!accountPubkey) {
+ setEventsIFollowListEvent(null)
+ setEventsIMutedListEvent(null)
+ return
+ }
+ const pk = accountPubkey.trim().toLowerCase()
+ const [fromIdbFollow, fromIdbMuted] = await Promise.all([
+ indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST),
+ indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST)
+ ])
+ if (fromIdbFollow) setEventsIFollowListEvent(fromIdbFollow)
+ if (fromIdbMuted) setEventsIMutedListEvent(fromIdbMuted)
+ }, [accountPubkey])
+
+ const refreshNotificationThreadListsFromRelays = useCallback(async () => {
+ if (!accountPubkey) {
+ setEventsIFollowListEvent(null)
+ setEventsIMutedListEvent(null)
+ return
+ }
+ const urls = await buildComprehensiveRelayList()
+ if (!urls.length) {
+ await hydrateFromStorage()
+ return
+ }
+ const pk = accountPubkey.trim().toLowerCase()
+ const [remoteFollow, remoteMuted] = await Promise.all([
+ fetchLatestReplaceableListEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, urls),
+ fetchLatestReplaceableListEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, urls)
+ ])
+ const [idbFollow, idbMuted] = await Promise.all([
+ indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST),
+ indexedDb.getReplaceableEvent(pk, ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST)
+ ])
+ const pick = (remote: Event | undefined, idb: Event | null | undefined) => {
+ if (remote && idb) return remote.created_at >= idb.created_at ? remote : idb
+ return remote ?? idb ?? null
+ }
+ 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)
+ }
+ }, [accountPubkey, buildComprehensiveRelayList, hydrateFromStorage])
+
+ useEffect(() => {
+ void hydrateFromStorage()
+ }, [hydrateFromStorage])
+
+ useEffect(() => {
+ if (!accountPubkey) {
+ setEventsIFollowListEvent(null)
+ setEventsIMutedListEvent(null)
+ return
+ }
+ let cancelled = false
+ void (async () => {
+ if (cancelled) return
+ await refreshNotificationThreadListsFromRelays()
+ })()
+ return () => {
+ cancelled = true
+ }
+ }, [accountPubkey, refreshNotificationThreadListsFromRelays])
+
+ const followRefs = useMemo(
+ () => parseThreadWatchListRefs(eventsIFollowListEvent),
+ [eventsIFollowListEvent]
+ )
+ const mutedRefs = useMemo(
+ () => parseThreadWatchListRefs(eventsIMutedListEvent),
+ [eventsIMutedListEvent]
+ )
+
+ const isFollowedForNotifications = useCallback(
+ (event: Event) => threadWatchMatchesRefs(event, followRefs),
+ [followRefs]
+ )
+ const isMutedForNotifications = useCallback(
+ (event: Event) => threadWatchMatchesRefs(event, mutedRefs),
+ [mutedRefs]
+ )
+
+ const publishList = useCallback(
+ async (kind: number, nextTags: string[][], content: string) => {
+ if (!accountPubkey) return
+ const comprehensiveRelays = await buildComprehensiveRelayList()
+ const draft = createReplaceablePersonalListDraftEvent(kind, nextTags, content)
+ const ev = await publish(draft, { specifiedRelayUrls: comprehensiveRelays })
+ const stored = await indexedDb.putReplaceableEvent(ev)
+ if (kind === ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST) {
+ setEventsIFollowListEvent(stored)
+ } else {
+ setEventsIMutedListEvent(stored)
+ }
+ },
+ [accountPubkey, buildComprehensiveRelayList, publish]
+ )
+
+ const followThreadForNotifications = useCallback(
+ async (event: Event) => {
+ if (!accountPubkey) return
+ const comprehensiveRelays = await buildComprehensiveRelayList()
+ 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
+ }
+ 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 mutedStripped = mutedEv ? listTagsAfterRemovingThreadWatchMatches(mutedEv.tags, event) : null
+ if (mutedStripped) {
+ await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, mutedStripped, mutedEv.content)
+ }
+
+ const curTags = followEv?.tags ?? []
+ const curFollowRefs = parseThreadWatchListRefs(followEv)
+ if (threadWatchMatchesRefs(event, curFollowRefs)) {
+ return
+ }
+ const next = mergeTagsPreservingMeta(curTags, [refTag])
+ await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv?.content ?? '')
+ logger.component('NotificationThreadWatchProvider', 'follow thread for notifications', {
+ kind: event.kind
+ })
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const muteThreadForNotifications = useCallback(
+ async (event: Event) => {
+ if (!accountPubkey) return
+ const comprehensiveRelays = await buildComprehensiveRelayList()
+ 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
+ }
+ 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 followStripped = followEv ? listTagsAfterRemovingThreadWatchMatches(followEv.tags, event) : null
+ if (followStripped) {
+ await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, followStripped, followEv.content)
+ }
+
+ const curTags = mutedEv?.tags ?? []
+ const curMutedRefs = parseThreadWatchListRefs(mutedEv)
+ if (threadWatchMatchesRefs(event, curMutedRefs)) {
+ return
+ }
+ const next = mergeTagsPreservingMeta(curTags, [refTag])
+ await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv?.content ?? '')
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const unfollowThreadForNotifications = useCallback(
+ async (event: Event): Promise => {
+ 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)
+ if (!next) return false
+ await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content)
+ return true
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const unmuteThreadForNotifications = useCallback(
+ async (event: Event): Promise => {
+ 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)
+ if (!next) return false
+ await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content)
+ return true
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const removeFollowRefByBech32 = useCallback(
+ async (bech32Id: string): Promise => {
+ const ref = decodePersonalListBech32Ref(bech32Id)
+ if (!ref || !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 = listTagsWithoutRef(followEv.tags, ref)
+ if (!next) return false
+ await publishList(ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST, next, followEv.content)
+ return true
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const removeMuteRefByBech32 = useCallback(
+ async (bech32Id: string): Promise => {
+ const ref = decodePersonalListBech32Ref(bech32Id)
+ if (!ref || !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 = listTagsWithoutRef(mutedEv.tags, ref)
+ if (!next) return false
+ await publishList(ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST, next, mutedEv.content)
+ return true
+ },
+ [accountPubkey, buildComprehensiveRelayList, publishList]
+ )
+
+ const value = useMemo(
+ () => ({
+ eventsIFollowListEvent,
+ eventsIMutedListEvent,
+ followRefs,
+ mutedRefs,
+ isFollowedForNotifications,
+ isMutedForNotifications,
+ followThreadForNotifications,
+ muteThreadForNotifications,
+ unfollowThreadForNotifications,
+ unmuteThreadForNotifications,
+ refreshNotificationThreadListsFromRelays,
+ removeFollowRefByBech32,
+ removeMuteRefByBech32
+ }),
+ [
+ eventsIFollowListEvent,
+ eventsIMutedListEvent,
+ followRefs,
+ mutedRefs,
+ isFollowedForNotifications,
+ isMutedForNotifications,
+ followThreadForNotifications,
+ muteThreadForNotifications,
+ unfollowThreadForNotifications,
+ unmuteThreadForNotifications,
+ refreshNotificationThreadListsFromRelays,
+ removeFollowRefByBech32,
+ removeMuteRefByBech32
+ ]
+ )
+
+ return (
+ {children}
+ )
+}
+
+export function useNotificationThreadWatch(): TNotificationThreadWatchContext {
+ const ctx = useContext(NotificationThreadWatchContext)
+ if (!ctx) {
+ throw new Error('useNotificationThreadWatch must be used within NotificationThreadWatchProvider')
+ }
+ return ctx
+}
+
+export function useNotificationThreadWatchOptional(): TNotificationThreadWatchContext | undefined {
+ return useContext(NotificationThreadWatchContext)
+}
diff --git a/src/routes.tsx b/src/routes.tsx
index 5da100a0..dd6078f0 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -13,6 +13,16 @@ const FollowingListPageLazy = lazy(() => import('./pages/secondary/FollowingList
const GeneralSettingsPageLazy = lazy(() => import('./pages/secondary/GeneralSettingsPage'))
const MuteListPageLazy = lazy(() => import('./pages/secondary/MuteListPage'))
const BookmarkListPageLazy = lazy(() => import('./pages/secondary/BookmarkListPage'))
+const NotificationThreadFollowListPageLazy = lazy(() =>
+ import('./pages/secondary/NotificationThreadWatchListPage').then((m) => ({
+ default: m.NotificationThreadFollowListPage
+ }))
+)
+const NotificationThreadMuteListPageLazy = lazy(() =>
+ import('./pages/secondary/NotificationThreadWatchListPage').then((m) => ({
+ default: m.NotificationThreadMuteListPage
+ }))
+)
const PinListPageLazy = lazy(() => import('./pages/secondary/PinListPage'))
const InterestListPageLazy = lazy(() => import('./pages/secondary/InterestListPage'))
const NoteListPageLazy = lazy(() => import('./pages/secondary/NoteListPage'))
@@ -91,6 +101,8 @@ const ROUTES = [
{ path: '/profile-editor', element: SR(ProfileEditorPageLazy) },
{ path: '/mutes', element: SR(MuteListPageLazy) },
{ path: '/bookmarks', element: SR(BookmarkListPageLazy) },
+ { path: '/notification-thread-follow', element: SR(NotificationThreadFollowListPageLazy) },
+ { path: '/notification-thread-mute', element: SR(NotificationThreadMuteListPageLazy) },
{ path: '/pins', element: SR(PinListPageLazy) },
{ path: '/interests', element: SR(InterestListPageLazy) },
{ path: '/user-emojis', element: SR(UserEmojiListPageLazy) },
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index 8a8770e3..8f4d338a 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -86,6 +86,10 @@ export const StoreNames = {
FOLLOW_SET_EVENTS: 'followSetEvents',
MUTE_LIST_EVENTS: 'muteListEvents',
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
+ /** Imwald kind 19130: thread roots to mirror in notifications. */
+ NOTIFICATION_THREAD_FOLLOW_EVENTS: 'notificationThreadFollowEvents',
+ /** Imwald kind 19132: thread roots to hide interaction notifications for. */
+ NOTIFICATION_THREAD_MUTE_EVENTS: 'notificationThreadMuteEvents',
PIN_LIST_EVENTS: 'pinListEvents',
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
INTEREST_LIST_EVENTS: 'interestListEvents',
@@ -168,7 +172,7 @@ const CACHE_BROWSER_EVENT_SEARCH_EXCLUDED_STORES: ReadonlySet = new Set(
])
/** Schema version we expect. When adding stores or migrations, bump this. */
-const DB_VERSION = 35
+const DB_VERSION = 36
/** Max age for profile and payment info cache before we refetch (5 min). */
const PROFILE_AND_PAYMENT_CACHE_MAX_AGE_MS = 5 * 60 * 1000
@@ -314,6 +318,12 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
}
+ if (!db.objectStoreNames.contains(StoreNames.NOTIFICATION_THREAD_FOLLOW_EVENTS)) {
+ db.createObjectStore(StoreNames.NOTIFICATION_THREAD_FOLLOW_EVENTS, { keyPath: 'key' })
+ }
+ if (!db.objectStoreNames.contains(StoreNames.NOTIFICATION_THREAD_MUTE_EVENTS)) {
+ db.createObjectStore(StoreNames.NOTIFICATION_THREAD_MUTE_EVENTS, { keyPath: 'key' })
+ }
if (!db.objectStoreNames.contains(StoreNames.PIN_LIST_EVENTS)) {
db.createObjectStore(StoreNames.PIN_LIST_EVENTS, { keyPath: 'key' })
}
@@ -1073,6 +1083,10 @@ class IndexedDbService {
return StoreNames.MUTE_LIST_EVENTS
case kinds.BookmarkList:
return StoreNames.BOOKMARK_LIST_EVENTS
+ case ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST:
+ return StoreNames.NOTIFICATION_THREAD_FOLLOW_EVENTS
+ case ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST:
+ return StoreNames.NOTIFICATION_THREAD_MUTE_EVENTS
case 10001: // Pin list
return StoreNames.PIN_LIST_EVENTS
case 10015: // Interest list
@@ -2182,6 +2196,10 @@ class IndexedDbService {
if (storeName === StoreNames.FOLLOW_SET_EVENTS) return ExtendedKind.FOLLOW_SET
if (storeName === StoreNames.MUTE_LIST_EVENTS) return kinds.Mutelist
if (storeName === StoreNames.BOOKMARK_LIST_EVENTS) return kinds.BookmarkList
+ if (storeName === StoreNames.NOTIFICATION_THREAD_FOLLOW_EVENTS)
+ return ExtendedKind.EVENTS_I_FOLLOW_NOTIFICATIONS_LIST
+ if (storeName === StoreNames.NOTIFICATION_THREAD_MUTE_EVENTS)
+ return ExtendedKind.EVENTS_I_MUTED_NOTIFICATIONS_LIST
if (storeName === StoreNames.PIN_LIST_EVENTS) return 10001
if (storeName === StoreNames.INTEREST_LIST_EVENTS) return 10015
if (storeName === StoreNames.BLOSSOM_SERVER_LIST_EVENTS) return ExtendedKind.BLOSSOM_SERVER_LIST
diff --git a/src/services/navigation.service.ts b/src/services/navigation.service.ts
index fb2464a9..f00459d4 100644
--- a/src/services/navigation.service.ts
+++ b/src/services/navigation.service.ts
@@ -46,6 +46,8 @@ export type ViewType =
| 'pins'
| 'interests'
| 'user-emojis'
+ | 'notification-thread-follow'
+ | 'notification-thread-mute'
| 'others-relay-settings'
| null
@@ -293,6 +295,8 @@ export class NavigationService {
if (viewType === 'following') return 'Following'
if (viewType === 'mute') return 'Muted Users'
if (viewType === 'bookmarks') return 'Bookmarks'
+ if (viewType === 'notification-thread-follow') return 'Thread notifications (follow)'
+ if (viewType === 'notification-thread-mute') return 'Thread notifications (mute)'
if (viewType === 'pins') return 'Pinned notes'
if (viewType === 'interests') return 'Interests'
if (viewType === 'user-emojis') return 'Custom emoji list'