Browse Source

feat: sync notifications read time

imwald
codytseng 11 months ago
parent
commit
30da0319ce
  1. 2
      src/components/BottomNavigationBar/NotificationsButton.tsx
  2. 3
      src/components/NotificationList/index.tsx
  3. 2
      src/components/Sidebar/NotificationButton.tsx
  4. 4
      src/constants.ts
  5. 11
      src/lib/draft-event.ts
  6. 57
      src/providers/NostrProvider/index.tsx
  7. 59
      src/providers/NotificationProvider.tsx

2
src/components/BottomNavigationBar/NotificationsButton.tsx

@ -15,7 +15,7 @@ export default function NotificationsButton() { @@ -15,7 +15,7 @@ export default function NotificationsButton() {
<div className="relative">
<Bell />
{hasNewNotification && (
<div className="absolute -top-0.5 right-0.5 w-2 h-2 bg-primary rounded-full" />
<div className="absolute -top-0.5 right-0.5 w-2 h-2 ring-2 ring-background bg-primary rounded-full" />
)}
</div>
</BottomNavigationBarItem>

3
src/components/NotificationList/index.tsx

@ -5,6 +5,7 @@ import { cn } from '@/lib/utils' @@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useNotification } from '@/providers/NotificationProvider'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import { TNotificationType } from '@/types'
@ -21,6 +22,7 @@ const SHOW_COUNT = 30 @@ -21,6 +22,7 @@ const SHOW_COUNT = 30
const NotificationList = forwardRef((_, ref) => {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { clearNewNotifications: updateReadNotificationTime } = useNotification()
const { updateNoteStatsByEvents } = useNoteStats()
const [notificationType, setNotificationType] = useState<TNotificationType>('all')
const [lastReadTime, setLastReadTime] = useState(0)
@ -67,6 +69,7 @@ const NotificationList = forwardRef((_, ref) => { @@ -67,6 +69,7 @@ const NotificationList = forwardRef((_, ref) => {
setNotifications([])
setShowCount(SHOW_COUNT)
setLastReadTime(storage.getLastReadNotificationTime(pubkey))
updateReadNotificationTime()
const relayList = await client.fetchRelayList(pubkey)
const { closer, timelineKey } = await client.subscribeTimeline(

2
src/components/Sidebar/NotificationButton.tsx

@ -16,7 +16,7 @@ export default function NotificationsButton() { @@ -16,7 +16,7 @@ export default function NotificationsButton() {
<div className="relative">
<Bell strokeWidth={3} />
{hasNewNotification && (
<div className="absolute -top-1 right-0 w-2 h-2 bg-primary rounded-full" />
<div className="absolute -top-1 right-0 w-2 h-2 ring-2 ring-background bg-primary rounded-full" />
)}
</div>
</SidebarItem>

4
src/constants.ts

@ -22,6 +22,10 @@ export const StorageKey = { @@ -22,6 +22,10 @@ export const StorageKey = {
FEED_TYPE: 'feedType' // deprecated
}
export const ApplicationDataKey = {
NOTIFICATIONS_SEEN_AT: 'seen_notifications_at'
}
export const BIG_RELAY_URLS = [
'wss://relay.damus.io/',
'wss://nos.lol/',

11
src/lib/draft-event.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { ExtendedKind } from '@/constants'
import { ApplicationDataKey, ExtendedKind } from '@/constants'
import client from '@/services/client.service'
import { TDraftEvent, TMailboxRelay, TRelaySet } from '@/types'
import dayjs from 'dayjs'
@ -274,6 +274,15 @@ export function createFavoriteRelaysDraftEvent( @@ -274,6 +274,15 @@ export function createFavoriteRelaysDraftEvent(
}
}
export function createSeenNotificationsAtDraftEvent(): TDraftEvent {
return {
kind: kinds.Application,
content: 'Records read time to sync notification status across devices.',
tags: [['d', ApplicationDataKey.NOTIFICATIONS_SEEN_AT]],
created_at: dayjs().unix()
}
}
function generateImetaTags(imageUrls: string[], pictureInfos: { url: string; tags: string[][] }[]) {
return imageUrls.map((imageUrl) => {
const pictureInfo = pictureInfos.find((info) => info.url === imageUrl)

57
src/providers/NostrProvider/index.tsx

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
import LoginDialog from '@/components/LoginDialog'
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { useToast } from '@/hooks'
import { createSeenNotificationsAtDraftEvent } from '@/lib/draft-event'
import {
getLatestEvent,
getProfileFromProfileEvent,
getRelayListFromRelayListEvent
getRelayListFromRelayListEvent,
getReplaceableEventIdentifier
} from '@/lib/event'
import { formatPubkey, isValidPubkey } from '@/lib/pubkey'
import client from '@/services/client.service'
@ -20,8 +22,8 @@ import { createContext, useContext, useEffect, useState } from 'react' @@ -20,8 +22,8 @@ import { createContext, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BunkerSigner } from './bunker.signer'
import { Nip07Signer } from './nip-07.signer'
import { NsecSigner } from './nsec.signer'
import { NpubSigner } from './npub.signer'
import { NsecSigner } from './nsec.signer'
type TNostrContext = {
isInitialized: boolean
@ -32,6 +34,7 @@ type TNostrContext = { @@ -32,6 +34,7 @@ type TNostrContext = {
followListEvent?: Event
muteListEvent?: Event
favoriteRelaysEvent: Event | null
notificationsSeenAt: number
account: TAccountPointer | null
accounts: TAccountPointer[]
nsec: string | null
@ -58,6 +61,7 @@ type TNostrContext = { @@ -58,6 +61,7 @@ type TNostrContext = {
updateFollowListEvent: (followListEvent: Event) => Promise<void>
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
updateNotificationsSeenAt: () => Promise<void>
}
const NostrContext = createContext<TNostrContext | undefined>(undefined)
@ -84,6 +88,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -84,6 +88,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
const [isInitialized, setIsInitialized] = useState(false)
useEffect(() => {
@ -183,15 +188,27 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -183,15 +188,27 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
setRelayList(relayList)
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), {
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), [
{
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist, ExtendedKind.FAVORITE_RELAYS],
authors: [account.pubkey]
})
},
{
kinds: [kinds.Application],
authors: [account.pubkey],
'#d': [ApplicationDataKey.NOTIFICATIONS_SEEN_AT]
}
])
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
const notificationsSeenAtEvent = sortedEvents.find(
(e) =>
e.kind === kinds.Application &&
getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
)
if (profileEvent) {
setProfileEvent(profileEvent)
setProfile(getProfileFromProfileEvent(profileEvent))
@ -215,6 +232,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -215,6 +232,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
}
const storedNotificationsSeenAt = storage.getLastReadNotificationTime(account.pubkey)
if (
notificationsSeenAtEvent &&
notificationsSeenAtEvent.created_at > storedNotificationsSeenAt
) {
setNotificationsSeenAt(notificationsSeenAtEvent.created_at)
storage.setLastReadNotificationTime(account.pubkey, notificationsSeenAtEvent.created_at)
} else {
setNotificationsSeenAt(storedNotificationsSeenAt)
}
client.initUserIndexFromFollowings(account.pubkey, controller.signal)
return controller
}
@ -429,6 +457,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -429,6 +457,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
draftEvent: TDraftEvent,
{ specifiedRelayUrls }: { specifiedRelayUrls?: string[] } = {}
) => {
if (!account || !signer || account.signerType === 'npub') {
throw new Error('You need to login first')
}
const additionalRelayUrls: string[] = []
if (
!specifiedRelayUrls?.length &&
@ -538,6 +570,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -538,6 +570,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const updateNotificationsSeenAt = async () => {
if (!account) return
const now = dayjs().unix()
storage.setLastReadNotificationTime(account.pubkey, now)
setTimeout(() => {
setNotificationsSeenAt(now)
}, 5_000)
await publish(createSeenNotificationsAtDraftEvent())
}
return (
<NostrContext.Provider
value={{
@ -549,6 +592,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -549,6 +592,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
followListEvent,
muteListEvent,
favoriteRelaysEvent,
notificationsSeenAt,
account,
accounts: storage
.getAccounts()
@ -573,7 +617,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -573,7 +617,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
updateProfileEvent,
updateFollowListEvent,
updateMuteListEvent,
updateFavoriteRelaysEvent
updateFavoriteRelaysEvent,
updateNotificationsSeenAt
}}
>
{children}

59
src/providers/NotificationProvider.tsx

@ -1,16 +1,14 @@ @@ -1,16 +1,14 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import dayjs from 'dayjs'
import { kinds } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { createContext, useContext, useEffect, useState } from 'react'
import { useMuteList } from './MuteListProvider'
import { useNostr } from './NostrProvider'
type TNotificationContext = {
hasNewNotification: boolean
clearNewNotifications: () => Promise<void>
}
const NotificationContext = createContext<TNotificationContext | undefined>(undefined)
@ -24,38 +22,14 @@ export const useNotification = () => { @@ -24,38 +22,14 @@ export const useNotification = () => {
}
export function NotificationProvider({ children }: { children: React.ReactNode }) {
const { pubkey } = useNostr()
const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr()
const { mutePubkeys } = useMuteList()
const { current } = usePrimaryPage()
const [hasNewNotification, setHasNewNotification] = useState(false)
const [lastReadTime, setLastReadTime] = useState(-1)
const previousPageRef = useRef<TPrimaryPageName | null>(null)
useEffect(() => {
if (current !== 'notifications' && previousPageRef.current === 'notifications') {
// navigate from notifications to other pages
setLastReadTime(dayjs().unix())
setHasNewNotification(false)
} else if (current === 'notifications' && previousPageRef.current !== null) {
// navigate to notifications
setHasNewNotification(false)
}
previousPageRef.current = current
}, [current])
useEffect(() => {
if (!pubkey || lastReadTime < 0) return
storage.setLastReadNotificationTime(pubkey, lastReadTime)
}, [lastReadTime])
if (!pubkey || notificationsSeenAt < 0) return
useEffect(() => {
if (!pubkey) return
setLastReadTime(storage.getLastReadNotificationTime(pubkey))
setHasNewNotification(false)
}, [pubkey])
useEffect(() => {
if (!pubkey || lastReadTime < 0) return
// Track if component is mounted
const isMountedRef = { current: true }
@ -79,7 +53,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -79,7 +53,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
kinds.Zap
],
'#p': [pubkey],
since: lastReadTime ?? dayjs().unix(),
since: notificationsSeenAt,
limit: 10
}
],
@ -102,7 +76,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -102,7 +76,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
if (isMountedRef.current) {
subscribe()
}
}, 5000)
}, 5_000)
}
}
}
@ -119,7 +93,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -119,7 +93,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
if (isMountedRef.current) {
subscribe()
}
}, 5000)
}, 5_000)
}
return null
}
@ -136,10 +110,25 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -136,10 +110,25 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
currentSubCloser = null
}
}
}, [lastReadTime, pubkey])
}, [notificationsSeenAt, pubkey])
useEffect(() => {
if (hasNewNotification) {
document.title = '📩 Jumble'
} else {
document.title = 'Jumble'
}
}, [hasNewNotification])
const clearNewNotifications = async () => {
if (!pubkey) return
setHasNewNotification(false)
await updateNotificationsSeenAt()
}
return (
<NotificationContext.Provider value={{ hasNewNotification }}>
<NotificationContext.Provider value={{ hasNewNotification, clearNewNotifications }}>
{children}
</NotificationContext.Provider>
)

Loading…
Cancel
Save