An ATproto social media client -- with an independent Appview.

[Notifications] Add a Mentions tab (#7044)

* Split out NotificationsTab

* Remove unused route parameter

* Refine the split between components

* Hoist some logic out of NotificationFeed

* Remove unused option

* Add all|conversations to query, hardcode "all"

* Add a Conversations tab

* Rename to Mentions

* Bump packages

* Rename fields

* Fix oopsie

* Simplify header

* Track active tab

* Fix types

* Separate logic for tabs

* Better border for first unread

* Highlight unread for all only

* Fix spinner races

* Fix fetchPage races

* Fix bottom bar border being obscured by glimmer

* Remember last tab within the session

* One tab at a time

* Fix TS

* Handle all RQKEY usages

* Nit

authored by danabra.mov and committed by

GitHub f8cdd6b9 10e241e7

+409 -267
+2 -2
package.json
··· 54 54 "icons:optimize": "svgo -f ./assets/icons" 55 55 }, 56 56 "dependencies": { 57 - "@atproto/api": "^0.13.18", 57 + "@atproto/api": "^0.13.20", 58 58 "@bitdrift/react-native": "0.4.0", 59 59 "@braintree/sanitize-url": "^6.0.2", 60 60 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", ··· 206 206 "zod": "^3.20.2" 207 207 }, 208 208 "devDependencies": { 209 - "@atproto/dev-env": "^0.3.64", 209 + "@atproto/dev-env": "^0.3.67", 210 210 "@babel/core": "^7.26.0", 211 211 "@babel/preset-env": "^7.26.0", 212 212 "@babel/runtime": "^7.26.0",
+11 -4
src/lib/hooks/useNotificationHandler.ts
··· 239 239 ) 240 240 logEvent('notifications:openApp', {}) 241 241 invalidateCachedUnreadPage() 242 - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) 242 + const payload = e.notification.request.trigger 243 + .payload as NotificationPayload 244 + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) 245 + if ( 246 + payload.reason === 'mention' || 247 + payload.reason === 'quote' || 248 + payload.reason === 'reply' 249 + ) { 250 + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) 251 + } 243 252 logger.debug('Notifications: handleNotification', { 244 253 content: e.notification.request.content, 245 254 payload: e.notification.request.trigger.payload, 246 255 }) 247 - handleNotification( 248 - e.notification.request.trigger.payload as NotificationPayload, 249 - ) 256 + handleNotification(payload) 250 257 Notifications.dismissAllNotificationsAsync() 251 258 } 252 259 })
+3 -3
src/lib/routes/types.ts
··· 75 75 } 76 76 77 77 export type NotificationsTabNavigatorParams = CommonNavigatorParams & { 78 - Notifications: {show?: 'all'} 78 + Notifications: undefined 79 79 } 80 80 81 81 export type MyProfileTabNavigatorParams = CommonNavigatorParams & { ··· 90 90 Home: undefined 91 91 Search: {q?: string} 92 92 Feeds: undefined 93 - Notifications: {show?: 'all'} 93 + Notifications: undefined 94 94 Hashtag: {tag: string; author?: string} 95 95 Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} 96 96 } ··· 102 102 Search: {q?: string} 103 103 Feeds: undefined 104 104 NotificationsTab: undefined 105 - Notifications: {show?: 'all'} 105 + Notifications: undefined 106 106 MyProfileTab: undefined 107 107 Hashtag: {tag: string; author?: string} 108 108 MessagesTab: undefined
+7 -1
src/screens/Settings/NotificationSettings.tsx
··· 18 18 export function NotificationSettingsScreen({}: Props) { 19 19 const {_} = useLingui() 20 20 21 - const {data, isError: isQueryError, refetch} = useNotificationFeedQuery() 21 + const { 22 + data, 23 + isError: isQueryError, 24 + refetch, 25 + } = useNotificationFeedQuery({ 26 + filter: 'all', 27 + }) 22 28 const serverPriority = data?.pages.at(0)?.priority 23 29 24 30 const {
+20 -14
src/state/queries/notifications/feed.ts
··· 52 52 type RQPageParam = string | undefined 53 53 54 54 const RQKEY_ROOT = 'notification-feed' 55 - export function RQKEY(priority?: false) { 56 - return [RQKEY_ROOT, priority] 55 + export function RQKEY(filter: 'all' | 'mentions') { 56 + return [RQKEY_ROOT, filter] 57 57 } 58 58 59 - export function useNotificationFeedQuery(opts?: { 59 + export function useNotificationFeedQuery(opts: { 60 60 enabled?: boolean 61 - overridePriorityNotifications?: boolean 61 + filter: 'all' | 'mentions' 62 62 }) { 63 63 const agent = useAgent() 64 64 const queryClient = useQueryClient() 65 65 const moderationOpts = useModerationOpts() 66 66 const unreads = useUnreadNotificationsApi() 67 - const enabled = opts?.enabled !== false 67 + const enabled = opts.enabled !== false 68 + const filter = opts.filter 68 69 const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris() 69 - 70 - // false: force showing all notifications 71 - // undefined: let the server decide 72 - const priority = opts?.overridePriorityNotifications ? false : undefined 73 70 74 71 const selectArgs = useMemo(() => { 75 72 return { ··· 91 88 RQPageParam 92 89 >({ 93 90 staleTime: STALE.INFINITY, 94 - queryKey: RQKEY(priority), 91 + queryKey: RQKEY(filter), 95 92 async queryFn({pageParam}: {pageParam: RQPageParam}) { 96 93 let page 97 - if (!pageParam) { 94 + if (filter === 'all' && !pageParam) { 98 95 // for the first page, we check the cached page held by the unread-checker first 99 96 page = unreads.getCachedUnreadPage() 100 97 } 101 98 if (!page) { 99 + let reasons: string[] = [] 100 + if (filter === 'mentions') { 101 + reasons = [ 102 + // Anything that's a post 103 + 'mention', 104 + 'reply', 105 + 'quote', 106 + ] 107 + } 102 108 const {page: fetchedPage} = await fetchPage({ 103 109 agent, 104 110 limit: PAGE_SIZE, ··· 106 112 queryClient, 107 113 moderationOpts, 108 114 fetchAdditionalData: true, 109 - priority, 115 + reasons, 110 116 }) 111 117 page = fetchedPage 112 118 } 113 119 114 - // if the first page has an unread, mark all read 115 - if (!pageParam) { 120 + if (filter === 'all' && !pageParam) { 121 + // if the first page has an unread, mark all read 116 122 unreads.markAllRead() 117 123 } 118 124
+6 -3
src/state/queries/notifications/settings.ts
··· 45 45 }, 46 46 onSettled: () => { 47 47 invalidateCachedUnreadPage() 48 - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) 48 + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) 49 + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) 49 50 }, 50 51 }) 51 52 } ··· 54 55 queryClient: ReturnType<typeof useQueryClient>, 55 56 enabled: boolean, 56 57 ) { 57 - queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => { 58 + function updateData(old: any) { 58 59 if (!old) return old 59 60 return { 60 61 ...old, ··· 65 66 } 66 67 }), 67 68 } 68 - }) 69 + } 70 + queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) 71 + queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) 69 72 }
+14 -2
src/state/queries/notifications/unread.tsx
··· 2 2 * A kind of companion API to ./feed.ts. See that file for more info. 3 3 */ 4 4 5 - import React from 'react' 5 + import React, {useRef} from 'react' 6 6 import {AppState} from 'react-native' 7 7 import {useQueryClient} from '@tanstack/react-query' 8 8 import EventEmitter from 'eventemitter3' ··· 105 105 } 106 106 }, [setNumUnread]) 107 107 108 + const isFetchingRef = useRef(false) 109 + 108 110 // create API 109 111 const api = React.useMemo<ApiContext>(() => { 110 112 return { ··· 138 140 } 139 141 } 140 142 143 + if (isFetchingRef.current) { 144 + return 145 + } 146 + // Do not move this without ensuring it gets a symmetrical reset in the finally block. 147 + isFetchingRef.current = true 148 + 141 149 // count 142 150 const {page, indexedAt: lastIndexed} = await fetchPage({ 143 151 agent, ··· 145 153 limit: 40, 146 154 queryClient, 147 155 moderationOpts, 156 + reasons: [], 148 157 149 158 // only fetch subjects when the page is going to be used 150 159 // in the notifications query, otherwise skip it ··· 174 183 // update & broadcast 175 184 setNumUnread(unreadCountStr) 176 185 if (invalidate) { 177 - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) 186 + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) 187 + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) 178 188 } 179 189 broadcast.postMessage({event: unreadCountStr}) 180 190 } catch (e) { 181 191 logger.warn('Failed to check unread notifications', {error: e}) 192 + } finally { 193 + isFetchingRef.current = false 182 194 } 183 195 }, 184 196
+3 -2
src/state/queries/notifications/util.ts
··· 31 31 queryClient, 32 32 moderationOpts, 33 33 fetchAdditionalData, 34 + reasons, 34 35 }: { 35 36 agent: BskyAgent 36 37 cursor: string | undefined ··· 38 39 queryClient: QueryClient 39 40 moderationOpts: ModerationOpts | undefined 40 41 fetchAdditionalData: boolean 41 - priority?: boolean 42 + reasons: string[] 42 43 }): Promise<{ 43 44 page: FeedPage 44 45 indexedAt: string | undefined ··· 46 47 const res = await agent.listNotifications({ 47 48 limit, 48 49 cursor, 49 - // priority, 50 + reasons, 50 51 }) 51 52 52 53 const indexedAt = res.data.notifications[0]?.indexedAt
+2 -2
src/state/queries/util.ts
··· 8 8 } from '@atproto/api' 9 9 import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query' 10 10 11 - export function truncateAndInvalidate<T = any>( 11 + export async function truncateAndInvalidate<T = any>( 12 12 queryClient: QueryClient, 13 13 queryKey: QueryKey, 14 14 ) { ··· 21 21 } 22 22 return data 23 23 }) 24 - queryClient.invalidateQueries({queryKey}) 24 + return queryClient.invalidateQueries({queryKey}) 25 25 } 26 26 27 27 // Given an AtUri, this function will check if the AtUri matches a
+14 -19
src/view/com/notifications/NotificationFeed.tsx
··· 9 9 import {useLingui} from '@lingui/react' 10 10 11 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 - import {usePalette} from '#/lib/hooks/usePalette' 13 12 import {cleanError} from '#/lib/strings/errors' 14 13 import {s} from '#/lib/styles' 15 14 import {logger} from '#/logger' 16 15 import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 16 import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' 18 - import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' 19 17 import {EmptyState} from '#/view/com/util/EmptyState' 20 18 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 21 19 import {List, ListRef} from '#/view/com/util/List' ··· 28 26 const LOADING_ITEM = {_reactKey: '__loading__'} 29 27 30 28 export function NotificationFeed({ 29 + filter, 30 + enabled, 31 31 scrollElRef, 32 32 onPressTryAgain, 33 33 onScrolledDownChange, 34 34 ListHeaderComponent, 35 - overridePriorityNotifications, 35 + refreshNotifications, 36 36 }: { 37 + filter: 'all' | 'mentions' 38 + enabled: boolean 37 39 scrollElRef?: ListRef 38 40 onPressTryAgain?: () => void 39 41 onScrolledDownChange: (isScrolledDown: boolean) => void 40 42 ListHeaderComponent?: () => JSX.Element 41 - overridePriorityNotifications?: boolean 43 + refreshNotifications: () => Promise<void> 42 44 }) { 43 45 const initialNumToRender = useInitialNumToRender() 44 - 45 46 const [isPTRing, setIsPTRing] = React.useState(false) 46 - const pal = usePalette('default') 47 - 48 47 const {_} = useLingui() 49 48 const moderationOpts = useModerationOpts() 50 - const {checkUnread} = useUnreadNotificationsApi() 51 49 const { 52 50 data, 53 51 isFetching, ··· 58 56 isFetchingNextPage, 59 57 fetchNextPage, 60 58 } = useNotificationFeedQuery({ 61 - enabled: !!moderationOpts, 62 - overridePriorityNotifications, 59 + enabled: enabled && !!moderationOpts, 60 + filter, 63 61 }) 64 62 const isEmpty = !isFetching && !data?.pages[0]?.items.length 65 63 ··· 85 83 const onRefresh = React.useCallback(async () => { 86 84 try { 87 85 setIsPTRing(true) 88 - await checkUnread({invalidate: true}) 86 + await refreshNotifications() 89 87 } catch (err) { 90 88 logger.error('Failed to refresh notifications feed', { 91 89 message: err, ··· 93 91 } finally { 94 92 setIsPTRing(false) 95 93 } 96 - }, [checkUnread, setIsPTRing]) 94 + }, [refreshNotifications, setIsPTRing]) 97 95 98 96 const onEndReached = React.useCallback(async () => { 99 97 if (isFetching || !hasNextPage || isError) return ··· 129 127 /> 130 128 ) 131 129 } else if (item === LOADING_ITEM) { 132 - return ( 133 - <View style={[pal.border]}> 134 - <NotificationFeedLoadingPlaceholder /> 135 - </View> 136 - ) 130 + return <NotificationFeedLoadingPlaceholder /> 137 131 } 138 132 return ( 139 133 <NotificationFeedItem 134 + highlightUnread={filter === 'all'} 140 135 item={item} 141 136 moderationOpts={moderationOpts!} 142 - hideTopBorder={index === 0} 137 + hideTopBorder={index === 0 && item.notification.isRead} 143 138 /> 144 139 ) 145 140 }, 146 - [moderationOpts, _, onPressRetryLoadMore, pal.border], 141 + [moderationOpts, _, onPressRetryLoadMore, filter], 147 142 ) 148 143 149 144 const FeedFooter = React.useCallback(
+7 -6
src/view/com/notifications/NotificationFeedItem.tsx
··· 79 79 let NotificationFeedItem = ({ 80 80 item, 81 81 moderationOpts, 82 + highlightUnread, 82 83 hideTopBorder, 83 84 }: { 84 85 item: FeedNotification 85 86 moderationOpts: ModerationOpts 87 + highlightUnread: boolean 86 88 hideTopBorder?: boolean 87 89 }): React.ReactNode => { 88 90 const queryClient = useQueryClient() ··· 151 153 if (!item.subject) { 152 154 return null 153 155 } 156 + const isHighlighted = highlightUnread && !item.notification.isRead 154 157 return ( 155 158 <Link 156 159 testID={`feedItem-by-${item.notification.author.handle}`} ··· 160 163 <Post 161 164 post={item.subject} 162 165 style={ 163 - item.notification.isRead 164 - ? undefined 165 - : { 166 - backgroundColor: pal.colors.unreadNotifBg, 167 - borderColor: pal.colors.unreadNotifBorder, 168 - } 166 + isHighlighted && { 167 + backgroundColor: pal.colors.unreadNotifBg, 168 + borderColor: pal.colors.unreadNotifBorder, 169 + } 169 170 } 170 171 hideTopBorder={hideTopBorder} 171 172 />
+8 -6
src/view/com/pager/Pager.tsx
··· 136 136 137 137 return ( 138 138 <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}> 139 - {renderTabBar({ 140 - selectedPage, 141 - onSelect: onTabBarSelect, 142 - dragProgress, 143 - dragState, 144 - })} 139 + <View style={a.z_10 /* Let tabbar bottom border cover the glimmer */}> 140 + {renderTabBar({ 141 + selectedPage, 142 + onSelect: onTabBarSelect, 143 + dragProgress, 144 + dragState, 145 + })} 146 + </View> 145 147 <GestureDetector gesture={nativeGesture}> 146 148 <AnimatedPagerView 147 149 ref={pagerView}
+7 -1
src/view/screens/DebugMod.tsx
··· 872 872 </P> 873 873 ) 874 874 } 875 - return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} /> 875 + return ( 876 + <NotificationFeedItem 877 + item={notif} 878 + moderationOpts={moderationOpts} 879 + highlightUnread 880 + /> 881 + ) 876 882 } 877 883 878 884 function MockAccountCard({
+175 -81
src/view/screens/Notifications.tsx
··· 13 13 } from '#/lib/routes/types' 14 14 import {s} from '#/lib/styles' 15 15 import {logger} from '#/logger' 16 - import {isNative, isWeb} from '#/platform/detection' 16 + import {isNative} from '#/platform/detection' 17 17 import {emitSoftReset, listenSoftReset} from '#/state/events' 18 18 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 19 19 import { ··· 24 24 import {useSetMinimalShellMode} from '#/state/shell' 25 25 import {useComposerControls} from '#/state/shell/composer' 26 26 import {NotificationFeed} from '#/view/com/notifications/NotificationFeed' 27 + import {Pager} from '#/view/com/pager/Pager' 28 + import {TabBar} from '#/view/com/pager/TabBar' 27 29 import {FAB} from '#/view/com/util/fab/FAB' 28 30 import {ListMethods} from '#/view/com/util/List' 29 31 import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' 30 32 import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' 31 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 32 - import {Button, ButtonIcon} from '#/components/Button' 33 + import {atoms as a} from '#/alf' 34 + import {web} from '#/alf' 35 + import {ButtonIcon} from '#/components/Button' 33 36 import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' 34 37 import * as Layout from '#/components/Layout' 35 38 import {Link} from '#/components/Link' 36 39 import {Loader} from '#/components/Loader' 37 40 41 + // We don't currently persist this across reloads since 42 + // you gotta visit All to clear the badge anyway. 43 + // But let's at least persist it during the sesssion. 44 + let lastActiveTab = 0 45 + 38 46 type Props = NativeStackScreenProps< 39 47 NotificationsTabNavigatorParams, 40 48 'Notifications' 41 49 > 42 - export function NotificationsScreen({route: {params}}: Props) { 43 - const t = useTheme() 44 - const {gtTablet} = useBreakpoints() 50 + export function NotificationsScreen({}: Props) { 51 + const {_} = useLingui() 52 + const {openComposer} = useComposerControls() 53 + const unreadNotifs = useUnreadNotifications() 54 + const hasNew = !!unreadNotifs 55 + const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi() 56 + const [isLoadingAll, setIsLoadingAll] = React.useState(false) 57 + const [isLoadingMentions, setIsLoadingMentions] = React.useState(false) 58 + const initialActiveTab = lastActiveTab 59 + const [activeTab, setActiveTab] = React.useState(initialActiveTab) 60 + const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions 61 + 62 + const onPageSelected = React.useCallback( 63 + (index: number) => { 64 + setActiveTab(index) 65 + lastActiveTab = index 66 + }, 67 + [setActiveTab], 68 + ) 69 + 70 + const queryClient = useQueryClient() 71 + const checkUnreadMentions = React.useCallback( 72 + async ({invalidate}: {invalidate: boolean}) => { 73 + if (invalidate) { 74 + return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions')) 75 + } else { 76 + // Background polling is not implemented for the mentions tab. 77 + // Just ignore it. 78 + } 79 + }, 80 + [queryClient], 81 + ) 82 + 83 + const sections = React.useMemo(() => { 84 + return [ 85 + { 86 + title: _(msg`All`), 87 + component: ( 88 + <NotificationsTab 89 + filter="all" 90 + isActive={activeTab === 0} 91 + isLoading={isLoadingAll} 92 + hasNew={hasNew} 93 + setIsLoadingLatest={setIsLoadingAll} 94 + checkUnread={checkUnreadAll} 95 + /> 96 + ), 97 + }, 98 + { 99 + title: _(msg`Mentions`), 100 + component: ( 101 + <NotificationsTab 102 + filter="mentions" 103 + isActive={activeTab === 1} 104 + isLoading={isLoadingMentions} 105 + hasNew={false /* We don't know for sure */} 106 + setIsLoadingLatest={setIsLoadingMentions} 107 + checkUnread={checkUnreadMentions} 108 + /> 109 + ), 110 + }, 111 + ] 112 + }, [ 113 + _, 114 + hasNew, 115 + checkUnreadAll, 116 + checkUnreadMentions, 117 + activeTab, 118 + isLoadingAll, 119 + isLoadingMentions, 120 + ]) 121 + 122 + return ( 123 + <Layout.Screen testID="notificationsScreen"> 124 + <Layout.Header.Outer noBottomBorder> 125 + <Layout.Header.MenuButton /> 126 + <Layout.Header.Content> 127 + <Layout.Header.TitleText> 128 + <Trans>Notifications</Trans> 129 + </Layout.Header.TitleText> 130 + </Layout.Header.Content> 131 + <Layout.Header.Slot> 132 + <Link 133 + to="/notifications/settings" 134 + label={_(msg`Notification settings`)} 135 + size="small" 136 + variant="ghost" 137 + color="secondary" 138 + shape="round" 139 + style={[a.justify_center]}> 140 + <ButtonIcon icon={isLoading ? Loader : SettingsIcon} size="lg" /> 141 + </Link> 142 + </Layout.Header.Slot> 143 + </Layout.Header.Outer> 144 + <Pager 145 + onPageSelected={onPageSelected} 146 + renderTabBar={props => ( 147 + <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}> 148 + <TabBar 149 + {...props} 150 + items={sections.map(section => section.title)} 151 + onPressSelected={() => emitSoftReset()} 152 + /> 153 + </Layout.Center> 154 + )} 155 + initialPage={initialActiveTab}> 156 + {sections.map((section, i) => ( 157 + <View key={i}>{section.component}</View> 158 + ))} 159 + </Pager> 160 + <FAB 161 + testID="composeFAB" 162 + onPress={() => openComposer({})} 163 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 164 + accessibilityRole="button" 165 + accessibilityLabel={_(msg`New post`)} 166 + accessibilityHint="" 167 + /> 168 + </Layout.Screen> 169 + ) 170 + } 171 + 172 + function NotificationsTab({ 173 + filter, 174 + isActive, 175 + isLoading, 176 + hasNew, 177 + checkUnread, 178 + setIsLoadingLatest, 179 + }: { 180 + filter: 'all' | 'mentions' 181 + isActive: boolean 182 + isLoading: boolean 183 + hasNew: boolean 184 + checkUnread: ({invalidate}: {invalidate: boolean}) => Promise<void> 185 + setIsLoadingLatest: (v: boolean) => void 186 + }) { 45 187 const {_} = useLingui() 46 188 const setMinimalShellMode = useSetMinimalShellMode() 47 189 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 48 - const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) 49 190 const scrollElRef = React.useRef<ListMethods>(null) 50 191 const queryClient = useQueryClient() 51 - const unreadNotifs = useUnreadNotifications() 52 - const unreadApi = useUnreadNotificationsApi() 53 - const hasNew = !!unreadNotifs 54 192 const isScreenFocused = useIsFocused() 55 - const {openComposer} = useComposerControls() 193 + const isFocusedAndActive = isScreenFocused && isActive 56 194 57 195 // event handlers 58 196 // = ··· 65 203 scrollToTop() 66 204 if (hasNew) { 67 205 // render what we have now 68 - truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) 69 - } else { 206 + truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter)) 207 + } else if (!isLoading) { 70 208 // check with the server 71 209 setIsLoadingLatest(true) 72 - unreadApi 73 - .checkUnread({invalidate: true}) 210 + checkUnread({invalidate: true}) 74 211 .catch(() => undefined) 75 212 .then(() => setIsLoadingLatest(false)) 76 213 } 77 - }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest]) 214 + }, [ 215 + scrollToTop, 216 + queryClient, 217 + checkUnread, 218 + hasNew, 219 + isLoading, 220 + setIsLoadingLatest, 221 + filter, 222 + ]) 78 223 79 224 const onFocusCheckLatest = useNonReactiveCallback(() => { 80 225 // on focus, check for latest, but only invalidate if the user ··· 87 232 // we're just going to look it up synchronously. 88 233 currentIsScrolledDown = window.scrollY > 200 89 234 } 90 - unreadApi.checkUnread({invalidate: !currentIsScrolledDown}) 235 + checkUnread({invalidate: !currentIsScrolledDown}) 91 236 }) 92 237 93 238 // on-visible setup 94 239 // = 95 240 useFocusEffect( 96 241 React.useCallback(() => { 97 - setMinimalShellMode(false) 98 - logger.debug('NotificationsScreen: Focus') 99 - onFocusCheckLatest() 100 - }, [setMinimalShellMode, onFocusCheckLatest]), 242 + if (isFocusedAndActive) { 243 + setMinimalShellMode(false) 244 + logger.debug('NotificationsScreen: Focus') 245 + onFocusCheckLatest() 246 + } 247 + }, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]), 101 248 ) 102 249 React.useEffect(() => { 103 - if (!isScreenFocused) { 250 + if (!isFocusedAndActive) { 104 251 return 105 252 } 106 253 return listenSoftReset(onPressLoadLatest) 107 - }, [onPressLoadLatest, isScreenFocused]) 254 + }, [onPressLoadLatest, isFocusedAndActive]) 108 255 109 256 return ( 110 - <Layout.Screen testID="notificationsScreen"> 111 - <Layout.Header.Outer> 112 - <Layout.Header.MenuButton /> 113 - <Layout.Header.Content> 114 - <Button 115 - label={_(msg`Notifications`)} 116 - accessibilityHint={_(msg`Refresh notifications`)} 117 - onPress={emitSoftReset} 118 - style={[a.justify_start]}> 119 - {({hovered}) => ( 120 - <Layout.Header.TitleText 121 - style={[a.w_full, hovered && a.underline]}> 122 - <Trans>Notifications</Trans> 123 - {isWeb && gtTablet && hasNew && ( 124 - <View 125 - style={[ 126 - a.rounded_full, 127 - { 128 - width: 8, 129 - height: 8, 130 - bottom: 3, 131 - left: 6, 132 - backgroundColor: t.palette.primary_500, 133 - }, 134 - ]} 135 - /> 136 - )} 137 - </Layout.Header.TitleText> 138 - )} 139 - </Button> 140 - </Layout.Header.Content> 141 - <Layout.Header.Slot> 142 - <Link 143 - to="/notifications/settings" 144 - label={_(msg`Notification settings`)} 145 - size="small" 146 - variant="ghost" 147 - color="secondary" 148 - shape="round" 149 - style={[a.justify_center]}> 150 - <ButtonIcon 151 - icon={isLoadingLatest ? Loader : SettingsIcon} 152 - size="lg" 153 - /> 154 - </Link> 155 - </Layout.Header.Slot> 156 - </Layout.Header.Outer> 157 - 257 + <> 158 258 <MainScrollProvider> 159 259 <NotificationFeed 260 + enabled={isFocusedAndActive} 261 + filter={filter} 262 + refreshNotifications={() => checkUnread({invalidate: true})} 160 263 onScrolledDownChange={setIsScrolledDown} 161 264 scrollElRef={scrollElRef} 162 - overridePriorityNotifications={params?.show === 'all'} 163 265 /> 164 266 </MainScrollProvider> 165 267 {(isScrolledDown || hasNew) && ( ··· 169 271 showIndicator={hasNew} 170 272 /> 171 273 )} 172 - <FAB 173 - testID="composeFAB" 174 - onPress={() => openComposer({})} 175 - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 176 - accessibilityRole="button" 177 - accessibilityLabel={_(msg`New post`)} 178 - accessibilityHint="" 179 - /> 180 - </Layout.Screen> 274 + </> 181 275 ) 182 276 }
+130 -121
yarn.lock
··· 20 20 "@jridgewell/gen-mapping" "^0.3.0" 21 21 "@jridgewell/trace-mapping" "^0.3.9" 22 22 23 - "@atproto-labs/fetch-node@0.1.3": 24 - version "0.1.3" 25 - resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.3.tgz#2581bf4710a4f957c74c75d959961de3304b3595" 26 - integrity sha512-KX3ogPJt6dXNppWImQ9omfhrc8t73WrJaxHMphRAqQL8jXxKW5NBCTjSuwroBkJ1pj1aValBrc5NpdYu+H/9Qg== 23 + "@atproto-labs/fetch-node@0.1.4": 24 + version "0.1.4" 25 + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch-node/-/fetch-node-0.1.4.tgz#03859a39556eab936e2b3bec2d087585c6408cb3" 26 + integrity sha512-hwYx0XpgIl2zydRy13DtWvywruuHk1EX+yCjqjgUIezUm8fi35ZN4QvR6INEm0MpN2MD/kQsImPbd8ZftzZ3zw== 27 27 dependencies: 28 - "@atproto-labs/fetch" "0.1.1" 28 + "@atproto-labs/fetch" "0.1.2" 29 29 "@atproto-labs/pipe" "0.1.0" 30 30 ipaddr.js "^2.1.0" 31 31 psl "^1.9.0" 32 32 undici "^6.14.1" 33 33 34 - "@atproto-labs/fetch@0.1.1": 35 - version "0.1.1" 36 - resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.1.tgz#10e7f8c06cf01a63f58e130b95d9ee0d4171902c" 37 - integrity sha512-X1zO1MDoJzEurbWXMAe1H8EZ995Xam/aXdxhGVrXmOMyPDuvBa1oxwh/kQNZRCKcMQUbiwkk+Jfq6ZkTuvGbww== 34 + "@atproto-labs/fetch@0.1.2": 35 + version "0.1.2" 36 + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.2.tgz#e1b9354205fb76f106ae3e1c6b56e7865a39600f" 37 + integrity sha512-7mQQIRtVenqtdBQKCqoLjyAhPS2aA56EGEjyz5zB3sramM3qkrvzyusr55GAzGDS0tvB6cy9cDEtSLmfK7LUnA== 38 38 dependencies: 39 39 "@atproto-labs/pipe" "0.1.0" 40 40 optionalDependencies: ··· 58 58 resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106" 59 59 integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg== 60 60 61 - "@atproto/api@^0.13.18": 62 - version "0.13.18" 63 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.18.tgz#cc537cc3b4c8d03f258a373f4d893fea11a77cdd" 64 - integrity sha512-rrl5HhzGYWZ7fiC965TPBUOVItq9M4dxMb6qz8IvAVQliSkrJrKc7UD0QWL89QiiXaOBuX8w+4i5r4wrfBGddg== 61 + "@atproto/api@^0.13.20": 62 + version "0.13.20" 63 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.20.tgz#5140db303c3b0981958dfe6a5fa6d7d1cd7bb3cc" 64 + integrity sha512-z/+CvG6BEttRHf856tKSe1AeUQNfrobRJldaHAthGmFk7O3wLZQyfcI9DUmBJQ9+4wAt0dZwvKWVGLZOV9eLHA== 65 65 dependencies: 66 66 "@atproto/common-web" "^0.3.1" 67 - "@atproto/lexicon" "^0.4.3" 67 + "@atproto/lexicon" "^0.4.4" 68 68 "@atproto/syntax" "^0.3.1" 69 - "@atproto/xrpc" "^0.6.4" 69 + "@atproto/xrpc" "^0.6.5" 70 70 await-lock "^2.2.2" 71 71 multiformats "^9.9.0" 72 72 tlds "^1.234.0" 73 73 zod "^3.23.8" 74 74 75 - "@atproto/aws@^0.2.9": 76 - version "0.2.9" 77 - resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.9.tgz#3539b281b725914b769451ee4afc62315dff1afc" 78 - integrity sha512-sc9aXUePcqItkJSOJJnGNVthVfAKjhn3zMDG+RRLzKUBye6Yutrlhpt1yxNZLHQiqIK5fy2Cuc4EX3p3jeWUYw== 75 + "@atproto/aws@^0.2.10": 76 + version "0.2.10" 77 + resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.10.tgz#e0b888fd50308cc24b7086cf3ec209587c13bbe4" 78 + integrity sha512-zQElKk6wGTQo5aKdXtmx/dINjkVgbJU9+C/xOVTs+M88I8IrrBxPvo1dASLJcMtRb9VjXh5snLJeAjgyx6qC6Q== 79 79 dependencies: 80 - "@atproto/common" "^0.4.4" 80 + "@atproto/common" "^0.4.5" 81 81 "@atproto/crypto" "^0.4.2" 82 - "@atproto/repo" "^0.5.5" 82 + "@atproto/repo" "^0.6.0" 83 83 "@aws-sdk/client-cloudfront" "^3.261.0" 84 84 "@aws-sdk/client-kms" "^3.196.0" 85 85 "@aws-sdk/client-s3" "^3.224.0" ··· 89 89 multiformats "^9.9.0" 90 90 uint8arrays "3.0.0" 91 91 92 - "@atproto/bsky@^0.0.96": 93 - version "0.0.96" 94 - resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.96.tgz#b89abf2828f57738357beb4efd05539667dd14b3" 95 - integrity sha512-Tk0ppiPMKdcnPU3x+uBAVRn92vroznhr2OlqinNSy/PZ39qWViRlKAhG3CLJsU2gjSHxsNfaIwulj7tPvKCmSw== 92 + "@atproto/bsky@^0.0.98": 93 + version "0.0.98" 94 + resolved "https://registry.yarnpkg.com/@atproto/bsky/-/bsky-0.0.98.tgz#4c4746e588568df1878647ae80cf4b963bc95924" 95 + integrity sha512-Y+un2pD1W1H0s0IWdY6S4vLy8rgR8cpqThz9onn4wDppmGWvOBNXeD8AjNzIWC0iFlYcfR4rwCKSoccUXYzxNg== 96 96 dependencies: 97 - "@atproto/api" "^0.13.18" 98 - "@atproto/common" "^0.4.4" 97 + "@atproto/api" "^0.13.20" 98 + "@atproto/common" "^0.4.5" 99 99 "@atproto/crypto" "^0.4.2" 100 100 "@atproto/identity" "^0.4.3" 101 - "@atproto/lexicon" "^0.4.3" 102 - "@atproto/repo" "^0.5.5" 103 - "@atproto/sync" "^0.1.6" 101 + "@atproto/lexicon" "^0.4.4" 102 + "@atproto/repo" "^0.6.0" 103 + "@atproto/sync" "^0.1.7" 104 104 "@atproto/syntax" "^0.3.1" 105 - "@atproto/xrpc-server" "^0.7.3" 105 + "@atproto/xrpc-server" "^0.7.4" 106 106 "@bufbuild/protobuf" "^1.5.0" 107 107 "@connectrpc/connect" "^1.1.4" 108 108 "@connectrpc/connect-express" "^1.1.4" ··· 129 129 typed-emitter "^2.1.0" 130 130 uint8arrays "3.0.0" 131 131 132 - "@atproto/bsync@^0.0.9": 133 - version "0.0.9" 134 - resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.9.tgz#7a6d58ef776404893d3c1139bdfe606fef483612" 135 - integrity sha512-N0+TnYOoJz4hTo6/h1jJKh6QzdbwkFuOQ1bdwugzST7ZkwMtjs5FX8o/uqgiD4gSHSqfQSRrew7+qYEHUT61Aw== 132 + "@atproto/bsync@^0.0.10": 133 + version "0.0.10" 134 + resolved "https://registry.yarnpkg.com/@atproto/bsync/-/bsync-0.0.10.tgz#fa16acfaf67112449b703778a20c785226c94189" 135 + integrity sha512-qviPMyYade/sqhX/9X9eTT4KaQ+FLvOyz+140LCDk/0vbZUCZPuYSEXZDCQkL5nlEXzScsQ3iyVeoYCGvV5kYw== 136 136 dependencies: 137 - "@atproto/common" "^0.4.4" 137 + "@atproto/common" "^0.4.5" 138 138 "@atproto/syntax" "^0.3.1" 139 139 "@bufbuild/protobuf" "^1.5.0" 140 140 "@connectrpc/connect" "^1.1.4" ··· 175 175 pino "^8.6.1" 176 176 zod "^3.14.2" 177 177 178 - "@atproto/common@^0.4.4": 179 - version "0.4.4" 180 - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.4.tgz#79096aef920f5ad7cda5c682d7ed7416d0581e1a" 181 - integrity sha512-58tMbn6A1Zu296s/l3uIj8z9d7IRHpZvLOfsFRikaQaYrzhJpL2aPY4uFQ8GJcxnsxeUnxBCrQz9we5jVVJI5Q== 178 + "@atproto/common@^0.4.5": 179 + version "0.4.5" 180 + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.4.5.tgz#28fd176a9b5527c723828e725586bc0be9fa9516" 181 + integrity sha512-LFAGqHcxCI5+b31Xgk+VQQtZU258iGPpHJzNeHVcdh6teIKZi4C2l6YV+m+3CEz+yYcfP7jjUmgqesx7l9Arsg== 182 182 dependencies: 183 183 "@atproto/common-web" "^0.3.1" 184 184 "@ipld/dag-cbor" "^7.0.3" ··· 207 207 "@noble/hashes" "^1.3.1" 208 208 uint8arrays "3.0.0" 209 209 210 - "@atproto/dev-env@^0.3.64": 211 - version "0.3.64" 212 - resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.64.tgz#148537785b6a86b0a56d0988e63a1ff8ea7c84e9" 213 - integrity sha512-s7mdppgp2BS0uy5ASZwqJ3J8dez14pDGI9uqTGbsOYF/qTCbBGZKw/Vkqjci5bY1UaW+o6n787q63ECDtljM8A== 210 + "@atproto/dev-env@^0.3.67": 211 + version "0.3.67" 212 + resolved "https://registry.yarnpkg.com/@atproto/dev-env/-/dev-env-0.3.67.tgz#4f6a20f0aafa8125ed9ec715abceedd11580882e" 213 + integrity sha512-7Ize4Y5vdjQjyrxTwjBPbkxKXQdE02KpE7AJLJt6Xpvowd2vbn8l8rDXfha+LtVi6t/613U4s+Slo5c1YD3x9A== 214 214 dependencies: 215 - "@atproto/api" "^0.13.18" 216 - "@atproto/bsky" "^0.0.96" 217 - "@atproto/bsync" "^0.0.9" 215 + "@atproto/api" "^0.13.20" 216 + "@atproto/bsky" "^0.0.98" 217 + "@atproto/bsync" "^0.0.10" 218 218 "@atproto/common-web" "^0.3.1" 219 219 "@atproto/crypto" "^0.4.2" 220 220 "@atproto/identity" "^0.4.3" 221 - "@atproto/lexicon" "^0.4.3" 222 - "@atproto/ozone" "^0.1.57" 223 - "@atproto/pds" "^0.4.73" 224 - "@atproto/sync" "^0.1.6" 221 + "@atproto/lexicon" "^0.4.4" 222 + "@atproto/ozone" "^0.1.59" 223 + "@atproto/pds" "^0.4.76" 224 + "@atproto/sync" "^0.1.7" 225 225 "@atproto/syntax" "^0.3.1" 226 - "@atproto/xrpc-server" "^0.7.3" 226 + "@atproto/xrpc-server" "^0.7.4" 227 227 "@did-plc/lib" "^0.0.1" 228 228 "@did-plc/server" "^0.0.1" 229 229 axios "^0.27.2" ··· 258 258 multiformats "^9.9.0" 259 259 zod "^3.23.8" 260 260 261 - "@atproto/lexicon@^0.4.3": 262 - version "0.4.3" 263 - resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.3.tgz#d69f6bb363a6326df7766c48132bfa30e22622d9" 264 - integrity sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg== 261 + "@atproto/lexicon@^0.4.4": 262 + version "0.4.4" 263 + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.4.tgz#0d97314bb57b693b76f2495fa5e02872469dd93a" 264 + integrity sha512-QFEmr3rpj/RoAmfX9ALU/asBG/rsVtQZnw+9nOB1/AuIwoxXd+ZyndR6lVUc2+DL4GEjl6W2yvBru5xbQIZWyA== 265 265 dependencies: 266 266 "@atproto/common-web" "^0.3.1" 267 267 "@atproto/syntax" "^0.3.1" ··· 269 269 multiformats "^9.9.0" 270 270 zod "^3.23.8" 271 271 272 - "@atproto/oauth-provider@^0.2.7": 273 - version "0.2.7" 274 - resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.7.tgz#38a211c197ee1ce4e92a5b59a92f2e15fcacee0b" 275 - integrity sha512-T/cEr7TGs36SqTW8JzLAt9EchumYY48zuI4rqoAepYT29eGpP37SxK+5X0+fQHOKJPKWUGlYocR9fDm4CdzAPQ== 272 + "@atproto/oauth-provider@^0.2.10": 273 + version "0.2.10" 274 + resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.2.10.tgz#f9820d7f82c33d3b74e81a75873f50e1e654b901" 275 + integrity sha512-cF42lo0+Mj+Zq2RXwS2NxmobmtL7YL1vXlYcN6iKflZ8pQ5WvpR/cZKsKEZOT9cEBBTw5MARKTYxbr8CPDKlHg== 276 276 dependencies: 277 - "@atproto-labs/fetch" "0.1.1" 278 - "@atproto-labs/fetch-node" "0.1.3" 277 + "@atproto-labs/fetch" "0.1.2" 278 + "@atproto-labs/fetch-node" "0.1.4" 279 279 "@atproto-labs/pipe" "0.1.0" 280 280 "@atproto-labs/simple-store" "0.1.1" 281 281 "@atproto-labs/simple-store-memory" "0.1.1" 282 - "@atproto/common" "^0.4.4" 282 + "@atproto/common" "^0.4.5" 283 283 "@atproto/jwk" "0.1.1" 284 284 "@atproto/jwk-jose" "0.1.2" 285 - "@atproto/oauth-types" "0.2.0" 285 + "@atproto/oauth-types" "0.2.1" 286 286 "@hapi/accept" "^6.0.3" 287 287 "@hapi/bourne" "^3.0.0" 288 288 "@hapi/content" "^6.0.0" ··· 294 294 psl "^1.9.0" 295 295 zod "^3.23.8" 296 296 297 - "@atproto/oauth-types@0.2.0": 298 - version "0.2.0" 299 - resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.0.tgz#28bc861b56cba093e6c52603cec1d3d38cd2a1e7" 300 - integrity sha512-v/4ht6eRh0yOu2iuuWujZdnJBamPKimdy8k0Xan8cVZ+a2i83UkhIIU+S/XUbbvJ4a64wLPZrS9IDd0K5XYYTQ== 297 + "@atproto/oauth-types@0.2.1": 298 + version "0.2.1" 299 + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.2.1.tgz#a7ace557cc91817fcde6195f023e4e1838e4aef6" 300 + integrity sha512-hDisUXzcq5KU1HMuCYZ8Kcz7BePl7V11bFjjgZvND3mdSphiyBpJ8MCNn3QzAa6cXpFo0w9PDcYMAlCCRZHdVw== 301 301 dependencies: 302 302 "@atproto/jwk" "0.1.1" 303 303 zod "^3.23.8" 304 304 305 - "@atproto/ozone@^0.1.57": 306 - version "0.1.57" 307 - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.57.tgz#141d66b213710575c7859d691586fd44c731f7ca" 308 - integrity sha512-P2YKeRFPbxKc2e2yftUoMTTcWYuFV0qU1/Nkd4GxuHnBnDJcbtMPglXd7kyLf0p8plCCFau/wZ8QdY8KSDLM9Q== 305 + "@atproto/ozone@^0.1.59": 306 + version "0.1.59" 307 + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.59.tgz#219984a46617b0ac039f2f02767290eaa0b4cfc3" 308 + integrity sha512-AD03Ocb3fZW+grxO/VwMld5iNdCLgbahFzku6xh1qEw0tLOBKp3GXSfepVd9XWu5fb1yPhGPd2JgjApV5hbJvw== 309 309 dependencies: 310 - "@atproto/api" "^0.13.18" 311 - "@atproto/common" "^0.4.4" 310 + "@atproto/api" "^0.13.20" 311 + "@atproto/common" "^0.4.5" 312 312 "@atproto/crypto" "^0.4.2" 313 313 "@atproto/identity" "^0.4.3" 314 - "@atproto/lexicon" "^0.4.3" 314 + "@atproto/lexicon" "^0.4.4" 315 315 "@atproto/syntax" "^0.3.1" 316 - "@atproto/xrpc" "^0.6.4" 317 - "@atproto/xrpc-server" "^0.7.3" 316 + "@atproto/xrpc" "^0.6.5" 317 + "@atproto/xrpc-server" "^0.7.4" 318 318 "@did-plc/lib" "^0.0.1" 319 319 axios "^1.6.7" 320 320 compression "^1.7.4" ··· 331 331 typed-emitter "^2.1.0" 332 332 uint8arrays "3.0.0" 333 333 334 - "@atproto/pds@^0.4.73": 335 - version "0.4.73" 336 - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.73.tgz#49b7625d9b40a5a24be1cdd7cdb56faab9e25707" 337 - integrity sha512-fzrKlgKVF5JvTTmhfvofXT9Ok1KFTfAjCzTrLJivbOcqQSqBagNTuz5CiQxAAAo/JTlSxmnyr3e7OrlJdrph1w== 334 + "@atproto/pds@^0.4.76": 335 + version "0.4.76" 336 + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.4.76.tgz#cd7b3f13359a7c31dc9362a5e4309419512c4102" 337 + integrity sha512-+cFVpqlgpCS0BuGac5fCQPZUugpS1r7ghnSQLVdjnTnvQJCqLRA++BlJWYbGgRP6FJrumCY2jtuwG8t59Rjt8Q== 338 338 dependencies: 339 - "@atproto-labs/fetch-node" "0.1.3" 340 - "@atproto/api" "^0.13.18" 341 - "@atproto/aws" "^0.2.9" 342 - "@atproto/common" "^0.4.4" 339 + "@atproto-labs/fetch-node" "0.1.4" 340 + "@atproto/api" "^0.13.20" 341 + "@atproto/aws" "^0.2.10" 342 + "@atproto/common" "^0.4.5" 343 343 "@atproto/crypto" "^0.4.2" 344 344 "@atproto/identity" "^0.4.3" 345 - "@atproto/lexicon" "^0.4.3" 346 - "@atproto/oauth-provider" "^0.2.7" 347 - "@atproto/repo" "^0.5.5" 345 + "@atproto/lexicon" "^0.4.4" 346 + "@atproto/oauth-provider" "^0.2.10" 347 + "@atproto/repo" "^0.6.0" 348 348 "@atproto/syntax" "^0.3.1" 349 - "@atproto/xrpc" "^0.6.4" 350 - "@atproto/xrpc-server" "^0.7.3" 349 + "@atproto/xrpc" "^0.6.5" 350 + "@atproto/xrpc-server" "^0.7.4" 351 351 "@did-plc/lib" "^0.0.4" 352 + "@hapi/address" "^5.1.1" 352 353 better-sqlite3 "^10.0.0" 353 354 bytes "^3.1.2" 354 355 compression "^1.7.4" 355 356 cors "^2.8.5" 356 - disposable-email "^0.2.3" 357 + disposable-email-domains-js "^1.5.0" 357 358 express "^4.17.2" 358 359 express-async-errors "^3.1.1" 359 360 file-type "^16.5.4" ··· 376 377 undici "^6.19.8" 377 378 zod "^3.23.8" 378 379 379 - "@atproto/repo@^0.5.5": 380 - version "0.5.5" 381 - resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.5.5.tgz#73eaf1a0b35cfc4fc1c837f4e3ddeb6768d29c20" 382 - integrity sha512-Zu1tw42KBVyFzIh1XYSIvm8V+V9oEKWJR7NnHBgeSMwCc9QwM32jO7uqgvEjZYEXgdYKanGhv/YHLyxtZa5Ckg== 380 + "@atproto/repo@^0.6.0": 381 + version "0.6.0" 382 + resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.6.0.tgz#29e698731e6df63636b0f7c91ce106a9de50ad19" 383 + integrity sha512-6YGVhjiHKmqCW5Ce4oY49E3NCEfbvAGowJ5ETXX2sx2l4D2bOL7a2hn5zWqsPHYpWSLjrPfnj7PVpApK0kmL7A== 383 384 dependencies: 384 - "@atproto/common" "^0.4.4" 385 + "@atproto/common" "^0.4.5" 385 386 "@atproto/common-web" "^0.3.1" 386 387 "@atproto/crypto" "^0.4.2" 387 - "@atproto/lexicon" "^0.4.3" 388 + "@atproto/lexicon" "^0.4.4" 388 389 "@ipld/car" "^3.2.3" 389 390 "@ipld/dag-cbor" "^7.0.0" 390 391 multiformats "^9.9.0" 391 392 uint8arrays "3.0.0" 392 393 zod "^3.23.8" 393 394 394 - "@atproto/sync@^0.1.6": 395 - version "0.1.6" 396 - resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.6.tgz#fb3e61147c05caf2c3d1cd597ff94fef68abbc02" 397 - integrity sha512-9lqe6E6fIns28TJyQufLCVefMxmK3bvEfQBhmXJBGZMHuKlH8+F5P9DfnHv6vs6ygfmHIUIjYDWqJu/rpt8pzw== 395 + "@atproto/sync@^0.1.7": 396 + version "0.1.7" 397 + resolved "https://registry.yarnpkg.com/@atproto/sync/-/sync-0.1.7.tgz#c7f78d99bb40eacf93ca13fdd04134a0985bf421" 398 + integrity sha512-liJH2EsD4AbWA8G0oRDURgbHW6Uq4NnM2rNfbrTlqgtj0kyGRY3FcVEyqeRcaQYfCuscChIg5DQKHqY421/7Mw== 398 399 dependencies: 399 - "@atproto/common" "^0.4.4" 400 + "@atproto/common" "^0.4.5" 400 401 "@atproto/identity" "^0.4.3" 401 - "@atproto/lexicon" "^0.4.3" 402 - "@atproto/repo" "^0.5.5" 402 + "@atproto/lexicon" "^0.4.4" 403 + "@atproto/repo" "^0.6.0" 403 404 "@atproto/syntax" "^0.3.1" 404 - "@atproto/xrpc-server" "^0.7.3" 405 + "@atproto/xrpc-server" "^0.7.4" 405 406 multiformats "^9.9.0" 406 407 p-queue "^6.6.2" 408 + ws "^8.12.0" 407 409 408 410 "@atproto/syntax@^0.3.1": 409 411 version "0.3.1" 410 412 resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.1.tgz#4346418728f9643d783d2ffcf7c77e132e1f53d4" 411 413 integrity sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw== 412 414 413 - "@atproto/xrpc-server@^0.7.3": 414 - version "0.7.3" 415 - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.3.tgz#d09b36d00edb7aacca48675d1ebb7fa796fa11bd" 416 - integrity sha512-x0qegkN6snrbXJO3v9h2kuh9e90g6ZZkDXv3COiraGS3yRTzIm6i4bMvDSfCI50+0xCNtPKOkpn8taRoRgkyiw== 415 + "@atproto/xrpc-server@^0.7.4": 416 + version "0.7.4" 417 + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.7.4.tgz#dfac8f7276c1c971a35eaba627eb6372088441c3" 418 + integrity sha512-MrAwxfJBQm/kCol3D8qc+vpQzBMzLqvtUbauSSfVVJ10PlGtxg4LlXqcjkAuhrjyrqp3dQH9LHuhDpgVQK+G3w== 417 419 dependencies: 418 - "@atproto/common" "^0.4.4" 420 + "@atproto/common" "^0.4.5" 419 421 "@atproto/crypto" "^0.4.2" 420 - "@atproto/lexicon" "^0.4.3" 421 - "@atproto/xrpc" "^0.6.4" 422 + "@atproto/lexicon" "^0.4.4" 423 + "@atproto/xrpc" "^0.6.5" 422 424 cbor-x "^1.5.1" 423 425 express "^4.17.2" 424 426 http-errors "^2.0.0" ··· 428 430 ws "^8.12.0" 429 431 zod "^3.23.8" 430 432 431 - "@atproto/xrpc@^0.6.4": 432 - version "0.6.4" 433 - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.4.tgz#4cf59774f7c72e5bc821bc5f1d57f0a6ae2014db" 434 - integrity sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA== 433 + "@atproto/xrpc@^0.6.5": 434 + version "0.6.5" 435 + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.5.tgz#8b180fc5f6b8374fd00c41b9e4cd7b24ead48e6b" 436 + integrity sha512-t6u8iPEVbWge5RhzKZDahSzNDYIAxUtop6Q/X/apAZY1rgreVU0/1sSvvRoRFH19d3UIKjYdLuwFqMi9w8nY3Q== 435 437 dependencies: 436 - "@atproto/lexicon" "^0.4.3" 438 + "@atproto/lexicon" "^0.4.4" 437 439 zod "^3.23.8" 438 440 439 441 "@aws-crypto/crc32@3.0.0": ··· 4376 4378 integrity sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw== 4377 4379 dependencies: 4378 4380 "@hapi/boom" "^10.0.1" 4381 + "@hapi/hoek" "^11.0.2" 4382 + 4383 + "@hapi/address@^5.1.1": 4384 + version "5.1.1" 4385 + resolved "https://registry.yarnpkg.com/@hapi/address/-/address-5.1.1.tgz#e9925fc1b65f5cc3fbea821f2b980e4652e84cb6" 4386 + integrity sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA== 4387 + dependencies: 4379 4388 "@hapi/hoek" "^11.0.2" 4380 4389 4381 4390 "@hapi/boom@^10.0.0", "@hapi/boom@^10.0.1": ··· 9593 9602 dependencies: 9594 9603 path-type "^4.0.0" 9595 9604 9596 - disposable-email@^0.2.3: 9597 - version "0.2.3" 9598 - resolved "https://registry.yarnpkg.com/disposable-email/-/disposable-email-0.2.3.tgz#a21a49717f6034a8ff777dc8eae3b4d994a7b988" 9599 - integrity sha512-gkBQQ5Res431ZXqLlAafrXHizG7/1FWmi8U2RTtriD78Vc10HhBUvdJun3R4eSF0KRIQQJs+wHlxjkED/Hr1EQ== 9605 + disposable-email-domains-js@^1.5.0: 9606 + version "1.7.0" 9607 + resolved "https://registry.yarnpkg.com/disposable-email-domains-js/-/disposable-email-domains-js-1.7.0.tgz#2bf859bccf7a2eb697025577e18f0434409713ec" 9608 + integrity sha512-qcIJcnXjDvH8EEt0tyAesk1sZVGU5ZFtW6Wys2wKCAcbUf5nJYfwZfT7Z0PVA/LBMlqd/Xgk9dXN2Q3fx7NFAg== 9600 9609 9601 9610 dns-equal@^1.0.0: 9602 9611 version "1.0.0"