Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at linkat-integration 240 lines 6.8 kB view raw
1/** 2 * A kind of companion API to ./feed.ts. See that file for more info. 3 */ 4 5import React, {useRef} from 'react' 6import {AppState} from 'react-native' 7import {useQueryClient} from '@tanstack/react-query' 8import EventEmitter from 'eventemitter3' 9 10import BroadcastChannel from '#/lib/broadcast' 11import {resetBadgeCount} from '#/lib/notifications/notifications' 12import {useModerationOpts} from '#/state/preferences/moderation-opts' 13import {truncateAndInvalidate} from '#/state/queries/util' 14import {useAgent, useSession} from '#/state/session' 15import {RQKEY as RQKEY_NOTIFS} from './feed' 16import {type CachedFeedPage, type FeedPage} from './types' 17import {fetchPage} from './util' 18 19const UPDATE_INTERVAL = 30 * 1e3 // 30sec 20 21const broadcast = new BroadcastChannel('NOTIFS_BROADCAST_CHANNEL') 22 23const emitter = new EventEmitter() 24 25type StateContext = string 26 27interface ApiContext { 28 markAllRead: () => Promise<void> 29 checkUnread: (opts?: { 30 invalidate?: boolean 31 isPoll?: boolean 32 }) => Promise<void> 33 getCachedUnreadPage: () => FeedPage | undefined 34} 35 36const stateContext = React.createContext<StateContext>('') 37stateContext.displayName = 'NotificationsUnreadStateContext' 38 39const apiContext = React.createContext<ApiContext>({ 40 async markAllRead() {}, 41 async checkUnread() {}, 42 getCachedUnreadPage: () => undefined, 43}) 44apiContext.displayName = 'NotificationsUnreadApiContext' 45 46export function Provider({children}: React.PropsWithChildren<{}>) { 47 const {hasSession} = useSession() 48 const agent = useAgent() 49 const queryClient = useQueryClient() 50 const moderationOpts = useModerationOpts() 51 52 const [numUnread, setNumUnread] = React.useState('') 53 54 const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null) 55 const cacheRef = React.useRef<CachedFeedPage>({ 56 usableInFeed: false, 57 syncedAt: new Date(), 58 data: undefined, 59 unreadCount: 0, 60 }) 61 62 React.useEffect(() => { 63 function markAsUnusable() { 64 if (cacheRef.current) { 65 cacheRef.current.usableInFeed = false 66 } 67 } 68 emitter.addListener('invalidate', markAsUnusable) 69 return () => { 70 emitter.removeListener('invalidate', markAsUnusable) 71 } 72 }, []) 73 74 // periodic sync 75 React.useEffect(() => { 76 if (!hasSession || !checkUnreadRef.current) { 77 return 78 } 79 checkUnreadRef.current() // fire on init 80 const interval = setInterval( 81 () => checkUnreadRef.current?.({isPoll: true}), 82 UPDATE_INTERVAL, 83 ) 84 return () => clearInterval(interval) 85 }, [hasSession]) 86 87 // listen for broadcasts 88 React.useEffect(() => { 89 const listener = ({data}: MessageEvent) => { 90 cacheRef.current = { 91 usableInFeed: false, 92 syncedAt: new Date(), 93 data: undefined, 94 unreadCount: 95 data.event === '30+' 96 ? 30 97 : data.event === '' 98 ? 0 99 : parseInt(data.event, 10) || 1, 100 } 101 setNumUnread(data.event) 102 } 103 broadcast.addEventListener('message', listener) 104 return () => { 105 broadcast.removeEventListener('message', listener) 106 } 107 }, [setNumUnread]) 108 109 const isFetchingRef = useRef(false) 110 111 // create API 112 const api = React.useMemo<ApiContext>(() => { 113 return { 114 async markAllRead() { 115 // update server 116 await agent.updateSeenNotifications( 117 cacheRef.current.syncedAt.toISOString(), 118 ) 119 120 // update & broadcast 121 setNumUnread('') 122 broadcast.postMessage({event: ''}) 123 resetBadgeCount() 124 }, 125 126 async checkUnread({ 127 invalidate, 128 isPoll, 129 }: {invalidate?: boolean; isPoll?: boolean} = {}) { 130 try { 131 if (!agent.session) return 132 if (AppState.currentState !== 'active') { 133 return 134 } 135 136 // reduce polling if unread count is set 137 if (isPoll && cacheRef.current?.unreadCount !== 0) { 138 // if hit 30+ then don't poll, otherwise reduce polling by 50% 139 if (cacheRef.current?.unreadCount >= 30 || Math.random() >= 0.5) { 140 return 141 } 142 } 143 144 if (isFetchingRef.current) { 145 return 146 } 147 // Do not move this without ensuring it gets a symmetrical reset in the finally block. 148 isFetchingRef.current = true 149 150 // count 151 const {page, indexedAt: lastIndexed} = await fetchPage({ 152 agent, 153 cursor: undefined, 154 limit: 40, 155 queryClient, 156 moderationOpts, 157 reasons: [], 158 159 // only fetch subjects when the page is going to be used 160 // in the notifications query, otherwise skip it 161 fetchAdditionalData: !!invalidate, 162 }) 163 const unreadCount = countUnread(page) 164 const unreadCountStr = 165 unreadCount >= 30 166 ? '30+' 167 : unreadCount === 0 168 ? '' 169 : String(unreadCount) 170 171 // track last sync 172 const now = new Date() 173 const lastIndexedDate = lastIndexed 174 ? new Date(lastIndexed) 175 : undefined 176 cacheRef.current = { 177 usableInFeed: !!invalidate, // will be used immediately 178 data: page, 179 syncedAt: 180 !lastIndexedDate || now > lastIndexedDate ? now : lastIndexedDate, 181 unreadCount, 182 } 183 184 // update & broadcast 185 setNumUnread(unreadCountStr) 186 if (invalidate) { 187 truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) 188 truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) 189 } 190 broadcast.postMessage({event: unreadCountStr}) 191 } finally { 192 isFetchingRef.current = false 193 } 194 }, 195 196 getCachedUnreadPage() { 197 // return cached page if it's marked as fresh enough 198 if (cacheRef.current.usableInFeed) { 199 return cacheRef.current.data 200 } 201 }, 202 } 203 }, [setNumUnread, queryClient, moderationOpts, agent]) 204 checkUnreadRef.current = api.checkUnread 205 206 return ( 207 <stateContext.Provider value={numUnread}> 208 <apiContext.Provider value={api}>{children}</apiContext.Provider> 209 </stateContext.Provider> 210 ) 211} 212 213export function useUnreadNotifications() { 214 return React.useContext(stateContext) 215} 216 217export function useUnreadNotificationsApi() { 218 return React.useContext(apiContext) 219} 220 221function countUnread(page: FeedPage) { 222 let num = 0 223 for (const item of page.items) { 224 if (!item.notification.isRead) { 225 num++ 226 } 227 if (item.additional) { 228 for (const item2 of item.additional) { 229 if (!item2.isRead) { 230 num++ 231 } 232 } 233 } 234 } 235 return num 236} 237 238export function invalidateCachedUnreadPage() { 239 emitter.emit('invalidate') 240}