Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

More notifications improvements (#2198)

* On mobile, never replace the notifs under the user due to focus events

* Use the server's seenAt response to calculate isRead state locally

authored by

Paul Frazee and committed by
GitHub
e3ba014b eecf0448

+42 -29
+1 -1
package.json
··· 35 35 "intl:compile": "lingui compile" 36 36 }, 37 37 "dependencies": { 38 - "@atproto/api": "^0.7.2", 38 + "@atproto/api": "^0.7.3", 39 39 "@bam.tech/react-native-image-resizer": "^3.0.4", 40 40 "@braintree/sanitize-url": "^6.0.2", 41 41 "@emoji-mart/react": "^1.1.1",
+18 -26
src/state/queries/notifications/feed.ts
··· 16 16 * 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead. 17 17 */ 18 18 19 - import {useEffect, useRef} from 'react' 19 + import {useEffect} from 'react' 20 20 import {AppBskyFeedDefs} from '@atproto/api' 21 21 import { 22 22 useInfiniteQuery, ··· 49 49 const threadMutes = useMutedThreads() 50 50 const unreads = useUnreadNotificationsApi() 51 51 const enabled = opts?.enabled !== false 52 - // state tracked across page fetches 53 - const pageState = useRef({pageNum: 0, hasMarkedRead: false}) 54 52 55 53 const query = useInfiniteQuery< 56 54 FeedPage, ··· 66 64 if (!pageParam) { 67 65 // for the first page, we check the cached page held by the unread-checker first 68 66 page = unreads.getCachedUnreadPage() 69 - // reset the page state 70 - pageState.current = {pageNum: 0, hasMarkedRead: false} 71 67 } 72 68 if (!page) { 73 69 page = await fetchPage({ ··· 80 76 }) 81 77 } 82 78 83 - // NOTE 84 - // this section checks to see if we need to mark notifs read 85 - // we want to wait until we've seen a read notification because 86 - // of a timing challenge; marking read on the first page would 87 - // cause subsequent pages of unread notifs to incorrectly come 88 - // back as "read". we use page 6 as an abort condition, which means 89 - // after ~180 notifs we give up on tracking unread state correctly 90 - // -prf 91 - if (!pageState.current.hasMarkedRead) { 92 - let hasMarkedRead = false 93 - if ( 94 - pageState.current.pageNum > 5 || 95 - page.items.some(item => item.notification.isRead) 96 - ) { 97 - unreads.markAllRead() 98 - hasMarkedRead = true 99 - } 100 - pageState.current = { 101 - pageNum: pageState.current.pageNum + 1, 102 - hasMarkedRead, 103 - } 79 + // if the first page has an unread, mark all read 80 + if (!pageParam && page.items[0] && !page.items[0].notification.isRead) { 81 + unreads.markAllRead() 104 82 } 105 83 106 84 return page ··· 108 86 initialPageParam: undefined, 109 87 getNextPageParam: lastPage => lastPage.cursor, 110 88 enabled, 89 + select(data: InfiniteData<FeedPage>) { 90 + // override 'isRead' using the first page's returned seenAt 91 + // we do this because the `markAllRead()` call above will 92 + // mark subsequent pages as read prematurely 93 + const seenAt = data.pages[0]?.seenAt || new Date() 94 + for (const page of data.pages) { 95 + for (const item of page.items) { 96 + item.notification.isRead = 97 + seenAt > new Date(item.notification.indexedAt) 98 + } 99 + } 100 + 101 + return data 102 + }, 111 103 }) 112 104 113 105 useEffect(() => {
+1
src/state/queries/notifications/types.ts
··· 24 24 25 25 export interface FeedPage { 26 26 cursor: string | undefined 27 + seenAt: Date 27 28 items: FeedNotification[] 28 29 } 29 30
+6
src/state/queries/notifications/util.ts
··· 68 68 notif => !isThreadMuted(notif, threadMutes), 69 69 ) 70 70 71 + let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() 72 + if (Number.isNaN(seenAt.getTime())) { 73 + seenAt = new Date() 74 + } 75 + 71 76 return { 72 77 cursor: res.data.cursor, 78 + seenAt, 73 79 items: notifsGrouped, 74 80 } 75 81 }
+2 -2
src/view/screens/Notifications.tsx
··· 67 67 const onFocusCheckLatest = React.useCallback(() => { 68 68 // on focus, check for latest, but only invalidate if the user 69 69 // isnt scrolled down to avoid moving content underneath them 70 - unreadApi.checkUnread({invalidate: !isScrolledDown}) 71 - }, [unreadApi, isScrolledDown]) 70 + unreadApi.checkUnread({invalidate: !isScrolledDown && isDesktop}) 71 + }, [unreadApi, isScrolledDown, isDesktop]) 72 72 checkLatestRef.current = onFocusCheckLatest 73 73 74 74 // on-visible setup
+14
yarn.lock
··· 48 48 typed-emitter "^2.1.0" 49 49 zod "^3.21.4" 50 50 51 + "@atproto/api@^0.7.3": 52 + version "0.7.3" 53 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.7.3.tgz#3224000353619970d5e397a157c6e189e195ef47" 54 + integrity sha512-fKU+W+S4kKxClE6IcPBHPZAjcyBYxG28S0FW/bv3T/ZYDkNxGzDV4xuoHOyEDGtB30slltl5U83njuuRZs5xtw== 55 + dependencies: 56 + "@atproto/common-web" "^0.2.3" 57 + "@atproto/lexicon" "^0.3.1" 58 + "@atproto/syntax" "^0.1.5" 59 + "@atproto/xrpc" "^0.4.1" 60 + multiformats "^9.9.0" 61 + tlds "^1.234.0" 62 + typed-emitter "^2.1.0" 63 + zod "^3.21.4" 64 + 51 65 "@atproto/aws@^0.1.6": 52 66 version "0.1.6" 53 67 resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.1.6.tgz#c6ecbfd92b325f3c5433688534d47f43358b415b"