Browse Source

dynamic subtopics cont.

imwald
Silberengel 5 months ago
parent
commit
a8bea51636
  1. 15
      src/App.tsx
  2. 33
      src/components/NotificationList/NotificationItem/DiscussionNotification.tsx
  3. 4
      src/components/NotificationList/NotificationItem/index.tsx
  4. 90
      src/components/TopicSubscribeButton/index.tsx
  5. 2
      src/constants.ts
  6. 2
      src/lib/discussion-topics.ts
  7. 9
      src/lib/draft-event.ts
  8. 29
      src/pages/primary/DiscussionsPage/index.tsx
  9. 155
      src/providers/InterestListProvider.tsx
  10. 20
      src/providers/NostrProvider/index.tsx
  11. 57
      src/providers/NotificationProvider.tsx
  12. 4
      src/services/client.service.ts
  13. 8
      src/services/indexed-db.service.ts

15
src/App.tsx

@ -8,6 +8,7 @@ import { DeletedEventProvider } from '@/providers/DeletedEventProvider' @@ -8,6 +8,7 @@ import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider'
import { FeedProvider } from '@/providers/FeedProvider'
import { FollowListProvider } from '@/providers/FollowListProvider'
import { InterestListProvider } from '@/providers/InterestListProvider'
import { KindFilterProvider } from '@/providers/KindFilterProvider'
import { MediaUploadServiceProvider } from '@/providers/MediaUploadServiceProvider'
import { MuteListProvider } from '@/providers/MuteListProvider'
@ -33,9 +34,10 @@ export default function App(): JSX.Element { @@ -33,9 +34,10 @@ export default function App(): JSX.Element {
<FavoriteRelaysProvider>
<FollowListProvider>
<MuteListProvider>
<UserTrustProvider>
<BookmarksProvider>
<FeedProvider>
<InterestListProvider>
<UserTrustProvider>
<BookmarksProvider>
<FeedProvider>
<ReplyProvider>
<MediaUploadServiceProvider>
<KindFilterProvider>
@ -46,9 +48,10 @@ export default function App(): JSX.Element { @@ -46,9 +48,10 @@ export default function App(): JSX.Element {
</KindFilterProvider>
</MediaUploadServiceProvider>
</ReplyProvider>
</FeedProvider>
</BookmarksProvider>
</UserTrustProvider>
</FeedProvider>
</BookmarksProvider>
</UserTrustProvider>
</InterestListProvider>
</MuteListProvider>
</FollowListProvider>
</FavoriteRelaysProvider>

33
src/components/NotificationList/NotificationItem/DiscussionNotification.tsx

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import { MessageCircle } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import Notification from './Notification'
export function DiscussionNotification({
notification,
isNew = false
}: {
notification: Event
isNew?: boolean
}) {
const { t } = useTranslation()
// Get the topic from t-tags
const topicTags = notification.tags.filter(tag => tag[0] === 't' && tag[1])
const topics = topicTags.map(tag => tag[1])
const topicString = topics.length > 0 ? topics.join(', ') : t('general')
return (
<Notification
notificationId={notification.id}
sender={notification.pubkey}
sentAt={notification.created_at}
description={t('started a discussion in {{topic}}', { topic: topicString })}
icon={<MessageCircle className="w-4 h-4 text-primary" />}
targetEvent={notification}
isNew={isNew}
showStats={false}
/>
)
}

4
src/components/NotificationList/NotificationItem/index.tsx

@ -6,6 +6,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -6,6 +6,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import { DiscussionNotification } from './DiscussionNotification'
import { MentionNotification } from './MentionNotification'
import { PollResponseNotification } from './PollResponseNotification'
import { PublicMessageNotification } from './PublicMessageNotification'
@ -41,6 +42,9 @@ export function NotificationItem({ @@ -41,6 +42,9 @@ export function NotificationItem({
])
if (!canShow) return null
if (notification.kind === 11) {
return <DiscussionNotification notification={notification} isNew={isNew} />
}
if (notification.kind === kinds.Reaction) {
return <ReactionNotification notification={notification} isNew={isNew} />
}

90
src/components/TopicSubscribeButton/index.tsx

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
import { Button } from '@/components/ui/button'
import { useInterestList } from '@/providers/InterestListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { Bell, BellOff, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
interface TopicSubscribeButtonProps {
topic: string
variant?: 'default' | 'outline' | 'ghost' | 'icon'
size?: 'default' | 'sm' | 'lg' | 'icon'
showLabel?: boolean
}
export default function TopicSubscribeButton({
topic,
variant = 'outline',
size = 'sm',
showLabel = true
}: TopicSubscribeButtonProps) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { isSubscribed, subscribe, unsubscribe, changing } = useInterestList()
if (!pubkey) {
return null
}
const subscribed = isSubscribed(topic)
const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
if (changing) return
if (subscribed) {
await unsubscribe(topic)
} else {
await subscribe(topic)
}
}
if (variant === 'icon' || !showLabel) {
return (
<Button
variant={subscribed ? 'default' : 'outline'}
size={size === 'icon' ? 'icon' : size}
onClick={handleClick}
disabled={changing}
title={subscribed ? t('Unsubscribe') : t('Subscribe')}
>
{changing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : subscribed ? (
<Bell className="h-4 w-4" fill="currentColor" />
) : (
<BellOff className="h-4 w-4" />
)}
</Button>
)
}
return (
<Button
variant={subscribed ? 'default' : variant}
size={size}
onClick={handleClick}
disabled={changing}
className="flex items-center gap-2"
>
{changing ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{subscribed ? t('Unsubscribing...') : t('Subscribing...')}
</>
) : subscribed ? (
<>
<Bell className="h-4 w-4" fill="currentColor" />
{t('Subscribed')}
</>
) : (
<>
<BellOff className="h-4 w-4" />
{t('Subscribe')}
</>
)}
</Button>
)
}

2
src/constants.ts

@ -157,7 +157,7 @@ export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ @@ -157,7 +157,7 @@ export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_-]+:/g
export const EMBEDDED_EVENT_REGEX = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
export const EMBEDDED_MENTION_REGEX = /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+)/g
export const HASHTAG_REGEX = /#[\p{L}\p{N}\p{M}_]+/gu
export const HASHTAG_REGEX = /#[a-zA-Z0-9_\u00C0-\u017F\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF]+/g
export const LN_INVOICE_REGEX = /(ln(?:bc|tb|bcrt))([0-9]+[munp]?)?1([02-9ac-hj-np-z]+)/g
export const EMOJI_REGEX =
/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA70}-\u{1FAFF}]|[\u{1F004}]|[\u{1F0CF}]|[\u{1F18E}]|[\u{3030}]|[\u{2B50}]|[\u{2B55}]|[\u{2934}-\u{2935}]|[\u{2B05}-\u{2B07}]|[\u{2B1B}-\u{2B1C}]|[\u{3297}]|[\u{3299}]|[\u{303D}]|[\u{00A9}]|[\u{00AE}]|[\u{2122}]|[\u{23E9}-\u{23EF}]|[\u{23F0}]|[\u{23F3}]|[\u{FE00}-\u{FE0F}]|[\u{200D}]/gu

2
src/lib/discussion-topics.ts

@ -69,6 +69,7 @@ export function analyzeThreadTopics( @@ -69,6 +69,7 @@ export function analyzeThreadTopics(
for (const thread of threads) {
const allTopics = extractAllTopics(thread)
// Find the primary topic (first match from available topics)
let primaryTopic = 'general'
for (const topic of allTopics) {
@ -118,6 +119,7 @@ export function getDynamicSubtopics( @@ -118,6 +119,7 @@ export function getDynamicSubtopics(
const subtopics: string[] = []
for (const [subtopic, npubs] of analysis.subtopics.entries()) {
if (npubs.size >= minNpubs) {
subtopics.push(subtopic)

9
src/lib/draft-event.ts

@ -452,6 +452,15 @@ export function createBookmarkDraftEvent(tags: string[][], content = ''): TDraft @@ -452,6 +452,15 @@ export function createBookmarkDraftEvent(tags: string[][], content = ''): TDraft
}
}
export function createInterestListDraftEvent(topics: string[], content = ''): TDraftEvent {
return {
kind: 10015,
content,
tags: topics.map(topic => ['t', topic]),
created_at: dayjs().unix()
}
}
export function createBlossomServerListDraftEvent(servers: string[]): TDraftEvent {
return {
kind: ExtendedKind.BLOSSOM_SERVER_LIST,

29
src/pages/primary/DiscussionsPage/index.tsx

@ -14,6 +14,7 @@ import ThreadSort, { SortOption } from '@/pages/primary/DiscussionsPage/ThreadSo @@ -14,6 +14,7 @@ import ThreadSort, { SortOption } from '@/pages/primary/DiscussionsPage/ThreadSo
import CreateThreadDialog, { DISCUSSION_TOPICS } from '@/pages/primary/DiscussionsPage/CreateThreadDialog'
import ViewToggle from '@/pages/primary/DiscussionsPage/ViewToggle'
import SubtopicFilter from '@/pages/primary/DiscussionsPage/SubtopicFilter'
import TopicSubscribeButton from '@/components/TopicSubscribeButton'
import { NostrEvent } from 'nostr-tools'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
@ -122,7 +123,6 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -122,7 +123,6 @@ const DiscussionsPage = forwardRef((_, ref) => {
setCustomVoteStats({}) // Clear custom stats when fetching
try {
// Fetch all kind 11 events (limit 100, newest first)
console.log('Fetching kind 11 events from relays:', relayUrls)
// Fetch recent kind 11 events (last 30 days)
const thirtyDaysAgo = Math.floor((Date.now() - (30 * 24 * 60 * 60 * 1000)) / 1000)
@ -168,14 +168,14 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -168,14 +168,14 @@ const DiscussionsPage = forwardRef((_, ref) => {
}, [fetchAllThreads])
// Analyze topics whenever threads change
useEffect(() => {
if (allThreads.length > 0) {
const analysis = analyzeThreadTopics(allThreads, availableTopicIds)
setTopicAnalysis(analysis)
} else {
setTopicAnalysis(new Map())
}
}, [allThreads, availableTopicIds])
useEffect(() => {
if (allThreads.length > 0) {
const analysis = analyzeThreadTopics(allThreads, availableTopicIds)
setTopicAnalysis(analysis)
} else {
setTopicAnalysis(new Map())
}
}, [allThreads, availableTopicIds])
// Update available subtopics when topic analysis or selected topic changes
useEffect(() => {
@ -526,9 +526,14 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -526,9 +526,14 @@ const DiscussionsPage = forwardRef((_, ref) => {
>
<div className="p-4 space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">
{t('Discussions')} - {selectedTopic === 'all' ? t('All Topics') : DISCUSSION_TOPICS.find(t => t.id === selectedTopic)?.label}
</h1>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">
{t('Discussions')} - {selectedTopic === 'all' ? t('All Topics') : DISCUSSION_TOPICS.find(t => t.id === selectedTopic)?.label}
</h1>
{selectedTopic !== 'all' && selectedTopic !== 'general' && (
<TopicSubscribeButton topic={selectedTopic} size="sm" />
)}
</div>
<div className="flex items-center gap-2">
{selectedTopic === 'all' && (
<ViewToggle

155
src/providers/InterestListProvider.tsx

@ -0,0 +1,155 @@ @@ -0,0 +1,155 @@
import { createInterestListDraftEvent } from '@/lib/draft-event'
import { normalizeTopic } from '@/lib/discussion-topics'
import client from '@/services/client.service'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useNostr } from './NostrProvider'
type TInterestListContext = {
subscribedTopics: Set<string>
changing: boolean
isSubscribed: (topic: string) => boolean
subscribe: (topic: string) => Promise<void>
unsubscribe: (topic: string) => Promise<void>
getSubscribedTopics: () => string[]
}
const InterestListContext = createContext<TInterestListContext | undefined>(undefined)
export const useInterestList = () => {
const context = useContext(InterestListContext)
if (!context) {
throw new Error('useInterestList must be used within an InterestListProvider')
}
return context
}
export function InterestListProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()
const { pubkey: accountPubkey, interestListEvent, publish, updateInterestListEvent } = useNostr()
const [topics, setTopics] = useState<string[]>([])
const subscribedTopics = useMemo(() => new Set(topics), [topics])
const [changing, setChanging] = useState(false)
useEffect(() => {
const updateTopics = () => {
if (!interestListEvent) {
setTopics([])
return
}
// Extract t-tags from the interest list
const topicTags = interestListEvent.tags
.filter(tag => tag[0] === 't' && tag[1])
.map(tag => normalizeTopic(tag[1]))
setTopics(topicTags)
}
updateTopics()
}, [interestListEvent])
const getSubscribedTopics = useCallback(() => {
return Array.from(subscribedTopics)
}, [subscribedTopics])
const isSubscribed = useCallback(
(topic: string): boolean => {
return subscribedTopics.has(normalizeTopic(topic))
},
[subscribedTopics]
)
const publishNewInterestListEvent = async (newTopics: string[]) => {
const newInterestListEvent = createInterestListDraftEvent(newTopics)
const publishedEvent = await publish(newInterestListEvent)
return publishedEvent
}
const subscribe = async (topic: string) => {
if (!accountPubkey || changing) return
const normalizedTopic = normalizeTopic(topic)
if (subscribedTopics.has(normalizedTopic)) {
return
}
setChanging(true)
try {
const interestListEvent = await client.fetchInterestListEvent(accountPubkey)
const currentTopics = interestListEvent
? interestListEvent.tags
.filter(tag => tag[0] === 't' && tag[1])
.map(tag => normalizeTopic(tag[1]))
: []
if (currentTopics.includes(normalizedTopic)) {
// Already subscribed
return
}
const newTopics = [...currentTopics, normalizedTopic]
const newInterestListEvent = await publishNewInterestListEvent(newTopics)
await updateInterestListEvent(newInterestListEvent)
toast.success(t('Subscribed to topic'))
} catch (error) {
console.error('Failed to subscribe to topic:', error)
toast.error(t('Failed to subscribe to topic') + ': ' + (error as Error).message)
} finally {
setChanging(false)
}
}
const unsubscribe = async (topic: string) => {
if (!accountPubkey || changing) return
const normalizedTopic = normalizeTopic(topic)
if (!subscribedTopics.has(normalizedTopic)) {
return
}
setChanging(true)
try {
const interestListEvent = await client.fetchInterestListEvent(accountPubkey)
if (!interestListEvent) return
const currentTopics = interestListEvent.tags
.filter(tag => tag[0] === 't' && tag[1])
.map(tag => normalizeTopic(tag[1]))
const newTopics = currentTopics.filter(t => t !== normalizedTopic)
if (newTopics.length === currentTopics.length) {
// Topic wasn't in the list
return
}
const newInterestListEvent = await publishNewInterestListEvent(newTopics)
await updateInterestListEvent(newInterestListEvent)
toast.success(t('Unsubscribed from topic'))
} catch (error) {
console.error('Failed to unsubscribe from topic:', error)
toast.error(t('Failed to unsubscribe from topic') + ': ' + (error as Error).message)
} finally {
setChanging(false)
}
}
return (
<InterestListContext.Provider
value={{
subscribedTopics,
changing,
isSubscribed,
subscribe,
unsubscribe,
getSubscribedTopics
}}
>
{children}
</InterestListContext.Provider>
)
}

20
src/providers/NostrProvider/index.tsx

@ -53,6 +53,7 @@ type TNostrContext = { @@ -53,6 +53,7 @@ type TNostrContext = {
followListEvent: Event | null
muteListEvent: Event | null
bookmarkListEvent: Event | null
interestListEvent: Event | null
favoriteRelaysEvent: Event | null
userEmojiListEvent: Event | null
notificationsSeenAt: number
@ -84,6 +85,7 @@ type TNostrContext = { @@ -84,6 +85,7 @@ type TNostrContext = {
updateFollowListEvent: (followListEvent: Event) => Promise<void>
updateMuteListEvent: (muteListEvent: Event, privateTags: string[][]) => Promise<void>
updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
updateInterestListEvent: (interestListEvent: Event) => Promise<void>
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
}
@ -117,6 +119,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -117,6 +119,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [followListEvent, setFollowListEvent] = useState<Event | null>(null)
const [muteListEvent, setMuteListEvent] = useState<Event | null>(null)
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
const [interestListEvent, setInterestListEvent] = useState<Event | null>(null)
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
@ -241,6 +244,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -241,6 +244,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
kinds.Contacts,
kinds.Mutelist,
kinds.BookmarkList,
10015, // Interest list
ExtendedKind.FAVORITE_RELAYS,
ExtendedKind.BLOSSOM_SERVER_LIST,
kinds.UserEmojiList
@ -258,6 +262,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -258,6 +262,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
const bookmarkListEvent = sortedEvents.find((e) => e.kind === kinds.BookmarkList)
const interestListEvent = sortedEvents.find((e) => e.kind === 10015)
const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
const blossomServerListEvent = sortedEvents.find(
(e) => e.kind === ExtendedKind.BLOSSOM_SERVER_LIST
@ -299,6 +304,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -299,6 +304,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setBookmarkListEvent(bookmarkListEvent)
}
}
if (interestListEvent) {
const updatedInterestListEvent = await indexedDb.putReplaceableEvent(interestListEvent)
if (updatedInterestListEvent.id === interestListEvent.id) {
setInterestListEvent(interestListEvent)
}
}
if (favoriteRelaysEvent) {
const updatedFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
if (updatedFavoriteRelaysEvent.id === favoriteRelaysEvent.id) {
@ -746,6 +757,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -746,6 +757,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setBookmarkListEvent(newBookmarkListEvent)
}
const updateInterestListEvent = async (interestListEvent: Event) => {
const newInterestListEvent = await indexedDb.putReplaceableEvent(interestListEvent)
if (newInterestListEvent.id !== interestListEvent.id) return
setInterestListEvent(newInterestListEvent)
}
const updateFavoriteRelaysEvent = async (favoriteRelaysEvent: Event) => {
const newFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
if (newFavoriteRelaysEvent.id !== favoriteRelaysEvent.id) return
@ -786,6 +804,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -786,6 +804,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
followListEvent,
muteListEvent,
bookmarkListEvent,
interestListEvent,
favoriteRelaysEvent,
userEmojiListEvent,
notificationsSeenAt,
@ -814,6 +833,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -814,6 +833,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
updateFollowListEvent,
updateMuteListEvent,
updateBookmarkListEvent,
updateInterestListEvent,
updateFavoriteRelaysEvent,
updateNotificationsSeenAt
}}

57
src/providers/NotificationProvider.tsx

@ -11,6 +11,7 @@ import { useContentPolicy } from './ContentPolicyProvider' @@ -11,6 +11,7 @@ import { useContentPolicy } from './ContentPolicyProvider'
import { useMuteList } from './MuteListProvider'
import { useNostr } from './NostrProvider'
import { useUserTrust } from './UserTrustProvider'
import { useInterestList } from './InterestListProvider'
type TNotificationContext = {
hasNewNotification: boolean
@ -36,6 +37,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -36,6 +37,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { getSubscribedTopics } = useInterestList()
const [newNotifications, setNewNotifications] = useState<NostrEvent[]>([])
const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set())
const filteredNewNotifications = useMemo(() => {
@ -87,18 +89,67 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -87,18 +89,67 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const subCloserRef: {
current: SubCloser | null
} = { current: null }
const topicSubCloserRef: {
current: SubCloser | null
} = { current: null }
const subscribe = async () => {
if (subCloserRef.current) {
subCloserRef.current.close()
subCloserRef.current = null
}
if (topicSubCloserRef.current) {
topicSubCloserRef.current.close()
topicSubCloserRef.current = null
}
if (!isMountedRef.current) return null
try {
let eosed = false
const relayList = await client.fetchRelayList(pubkey)
const notificationRelays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS
// Subscribe to subscribed topics (kind 11 discussions)
const subscribedTopics = getSubscribedTopics()
if (subscribedTopics.length > 0) {
let topicEosed = false
const topicSubCloser = client.subscribe(
notificationRelays,
[
{
kinds: [11], // Discussion threads
'#t': subscribedTopics,
limit: 10
}
],
{
oneose: (e) => {
if (e) {
topicEosed = e
}
},
onevent: (evt) => {
// Don't notify about our own threads
if (evt.pubkey !== pubkey) {
setNewNotifications((prev) => {
if (!topicEosed) {
return [evt, ...prev]
}
if (prev.length && compareEvents(prev[0], evt) >= 0) {
return prev
}
client.emitNewEvent(evt)
return [evt, ...prev]
})
}
}
}
)
topicSubCloserRef.current = topicSubCloser
}
// Regular notifications subscription
const subCloser = client.subscribe(
notificationRelays,
[
@ -187,8 +238,12 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -187,8 +238,12 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
subCloserRef.current.close()
subCloserRef.current = null
}
if (topicSubCloserRef.current) {
topicSubCloserRef.current.close()
topicSubCloserRef.current = null
}
}
}, [pubkey])
}, [pubkey, getSubscribedTopics])
useEffect(() => {
const newNotificationCount = filteredNewNotifications.length

4
src/services/client.service.ts

@ -1685,6 +1685,10 @@ class ClientService extends EventTarget { @@ -1685,6 +1685,10 @@ class ClientService extends EventTarget {
return this.fetchReplaceableEvent(pubkey, kinds.BookmarkList)
}
async fetchInterestListEvent(pubkey: string) {
return this.fetchReplaceableEvent(pubkey, 10015)
}
async fetchBlossomServerListEvent(pubkey: string) {
return await this.fetchReplaceableEvent(pubkey, ExtendedKind.BLOSSOM_SERVER_LIST)
}

8
src/services/indexed-db.service.ts

@ -16,6 +16,7 @@ const StoreNames = { @@ -16,6 +16,7 @@ const StoreNames = {
MUTE_LIST_EVENTS: 'muteListEvents',
BOOKMARK_LIST_EVENTS: 'bookmarkListEvents',
BLOSSOM_SERVER_LIST_EVENTS: 'blossomServerListEvents',
INTEREST_LIST_EVENTS: 'interestListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents',
EMOJI_SET_EVENTS: 'emojiSetEvents',
@ -70,6 +71,9 @@ class IndexedDbService { @@ -70,6 +71,9 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.BOOKMARK_LIST_EVENTS)) {
db.createObjectStore(StoreNames.BOOKMARK_LIST_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.INTEREST_LIST_EVENTS)) {
db.createObjectStore(StoreNames.INTEREST_LIST_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
db.createObjectStore(StoreNames.MUTE_DECRYPTED_TAGS, { keyPath: 'key' })
}
@ -447,6 +451,10 @@ class IndexedDbService { @@ -447,6 +451,10 @@ class IndexedDbService {
return StoreNames.FOLLOW_LIST_EVENTS
case kinds.Mutelist:
return StoreNames.MUTE_LIST_EVENTS
case kinds.BookmarkList:
return StoreNames.BOOKMARK_LIST_EVENTS
case 10015: // Interest list
return StoreNames.INTEREST_LIST_EVENTS
case ExtendedKind.BLOSSOM_SERVER_LIST:
return StoreNames.BLOSSOM_SERVER_LIST_EVENTS
case kinds.Relaysets:

Loading…
Cancel
Save