my fork of the bluesky client

Priority notifications (#4798)

* new settings screen

* bring back the spinner

* add experimental language

* fix typo, change leading

* integrate priority notifications API

* update package

* use refetch instead of invalidateQueries

* fix read-after-write issue by polling for update

* add spinner for initial load

* rm onmutate, it's overcomplicated

* set error state eagerly

* Change language in description

Co-authored-by: Hailey <me@haileyok.com>

* prettier

* add `Toggle.Platform`

* extract out mutation hook + error state

* rm useless cache mutation

* disambiguate isError and isPending

* rm unused isError

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>

authored by samuel.fm

Hailey
Samuel Newman
and committed by
GitHub
cfb8a316 9bd83936

+305 -84
+1
bskyweb/cmd/bskyweb/server.go
··· 197 197 e.GET("/search", server.WebGeneric) 198 198 e.GET("/feeds", server.WebGeneric) 199 199 e.GET("/notifications", server.WebGeneric) 200 + e.GET("/notifications/settings", server.WebGeneric) 200 201 e.GET("/lists", server.WebGeneric) 201 202 e.GET("/moderation", server.WebGeneric) 202 203 e.GET("/moderation/modlists", server.WebGeneric)
+1 -1
package.json
··· 52 52 "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" 53 53 }, 54 54 "dependencies": { 55 - "@atproto/api": "^0.12.23", 55 + "@atproto/api": "0.12.25", 56 56 "@bam.tech/react-native-image-resizer": "^3.0.4", 57 57 "@braintree/sanitize-url": "^6.0.2", 58 58 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+6
src/Navigation.tsx
··· 76 76 import {ModerationModlistsScreen} from './view/screens/ModerationModlists' 77 77 import {NotFoundScreen} from './view/screens/NotFound' 78 78 import {NotificationsScreen} from './view/screens/Notifications' 79 + import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings' 79 80 import {PostLikedByScreen} from './view/screens/PostLikedBy' 80 81 import {PostRepostedByScreen} from './view/screens/PostRepostedBy' 81 82 import {PostThreadScreen} from './view/screens/PostThread' ··· 323 324 name="MessagesSettings" 324 325 getComponent={() => MessagesSettingsScreen} 325 326 options={{title: title(msg`Chat settings`), requireAuth: true}} 327 + /> 328 + <Stack.Screen 329 + name="NotificationsSettings" 330 + getComponent={() => NotificationsSettingsScreen} 331 + options={{title: title(msg`Notification settings`), requireAuth: true}} 326 332 /> 327 333 <Stack.Screen 328 334 name="Feeds"
+1 -1
src/components/Lists.tsx
··· 189 189 return ( 190 190 <Error 191 191 title={errorTitle ?? _(msg`Oops!`)} 192 - message={errorMessage ?? _(`Something went wrong!`)} 192 + message={errorMessage ?? _(msg`Something went wrong!`)} 193 193 onRetry={onRetry} 194 194 onGoBack={onGoBack} 195 195 sideBorders={sideBorders}
+3
src/components/forms/Toggle.tsx
··· 2 2 import {Pressable, View, ViewStyle} from 'react-native' 3 3 import Animated, {LinearTransition} from 'react-native-reanimated' 4 4 5 + import {isNative} from '#/platform/detection' 5 6 import {HITSLOP_10} from 'lib/constants' 6 7 import { 7 8 atoms as a, ··· 459 460 </View> 460 461 ) 461 462 } 463 + 464 + export const Platform = isNative ? Switch : Checkbox
-5
src/components/icons/Gear.tsx
··· 1 - import {createSinglePathSVG} from './TEMPLATE' 2 - 3 - export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 - path: 'M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z', 5 - })
+6 -4
src/lib/async/until.ts
··· 1 1 import {timeout} from './timeout' 2 2 3 - export async function until( 3 + export async function until<T>( 4 4 retries: number, 5 5 delay: number, 6 - cond: (v: any, err: any) => boolean, 7 - fn: () => Promise<any>, 6 + cond: (v: T, err: any) => boolean, 7 + fn: () => Promise<T>, 8 8 ): Promise<boolean> { 9 9 while (retries > 0) { 10 10 try { ··· 13 13 return true 14 14 } 15 15 } catch (e: any) { 16 - if (cond(undefined, e)) { 16 + // TODO: change the type signature of cond to accept undefined 17 + // however this breaks every existing usage of until -sfn 18 + if (cond(undefined as unknown as T, e)) { 17 19 return true 18 20 } 19 21 }
+6 -9
src/lib/routes/types.ts
··· 42 42 Hashtag: {tag: string; author?: string} 43 43 MessagesConversation: {conversation: string; embed?: string} 44 44 MessagesSettings: undefined 45 + NotificationsSettings: undefined 45 46 Feeds: undefined 46 47 Start: {name: string; rkey: string} 47 48 StarterPack: {name: string; rkey: string; new?: boolean} 48 49 StarterPackShort: {code: string} 49 50 StarterPackWizard: undefined 50 - StarterPackEdit: { 51 - rkey?: string 52 - } 51 + StarterPackEdit: {rkey?: string} 53 52 } 54 53 55 54 export type BottomTabNavigatorParams = CommonNavigatorParams & { ··· 69 68 } 70 69 71 70 export type NotificationsTabNavigatorParams = CommonNavigatorParams & { 72 - Notifications: undefined 71 + Notifications: {show?: 'all'} 73 72 } 74 73 75 74 export type MyProfileTabNavigatorParams = CommonNavigatorParams & { ··· 84 83 Home: undefined 85 84 Search: {q?: string} 86 85 Feeds: undefined 87 - Notifications: undefined 86 + Notifications: {show?: 'all'} 88 87 Hashtag: {tag: string; author?: string} 89 88 Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} 90 89 } ··· 96 95 Search: {q?: string} 97 96 Feeds: undefined 98 97 NotificationsTab: undefined 99 - Notifications: undefined 98 + Notifications: {show?: 'all'} 100 99 MyProfileTab: undefined 101 100 Hashtag: {tag: string; author?: string} 102 101 MessagesTab: undefined ··· 105 104 StarterPack: {name: string; rkey: string; new?: boolean} 106 105 StarterPackShort: {code: string} 107 106 StarterPackWizard: undefined 108 - StarterPackEdit: { 109 - rkey?: string 110 - } 107 + StarterPackEdit: {rkey?: string} 111 108 } 112 109 113 110 // NOTE
+1
src/routes.ts
··· 5 5 Search: '/search', 6 6 Feeds: '/feeds', 7 7 Notifications: '/notifications', 8 + NotificationsSettings: '/notifications/settings', 8 9 Settings: '/settings', 9 10 LanguageSettings: '/settings/language', 10 11 Lists: '/lists',
+1
src/screens/Messages/Conversation/index.tsx
··· 106 106 title={_(msg`Something went wrong`)} 107 107 message={_(msg`We couldn't load this conversation`)} 108 108 onRetry={() => convoState.error.retry()} 109 + sideBorders={false} 109 110 /> 110 111 </CenteredView> 111 112 )
+1 -1
src/screens/Messages/List/index.tsx
··· 309 309 a.gap_lg, 310 310 a.px_lg, 311 311 a.pr_md, 312 - a.py_md, 312 + a.py_sm, 313 313 a.border_b, 314 314 t.atoms.border_contrast_low, 315 315 ]}>
+1 -1
src/screens/Messages/Settings.tsx
··· 107 107 a.rounded_md, 108 108 t.atoms.bg_contrast_25, 109 109 ]}> 110 - <Text style={[t.atoms.text_contrast_high]}> 110 + <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> 111 111 <Trans> 112 112 You can continue ongoing conversations regardless of which setting 113 113 you choose.
+22 -15
src/state/queries/notifications/feed.ts
··· 46 46 type RQPageParam = string | undefined 47 47 48 48 const RQKEY_ROOT = 'notification-feed' 49 - export function RQKEY() { 50 - return [RQKEY_ROOT] 49 + export function RQKEY(priority?: false) { 50 + return [RQKEY_ROOT, priority] 51 51 } 52 52 53 - export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { 53 + export function useNotificationFeedQuery(opts?: { 54 + enabled?: boolean 55 + overridePriorityNotifications?: boolean 56 + }) { 54 57 const agent = useAgent() 55 58 const queryClient = useQueryClient() 56 59 const moderationOpts = useModerationOpts() ··· 59 62 const lastPageCountRef = useRef(0) 60 63 const gate = useGate() 61 64 65 + // false: force showing all notifications 66 + // undefined: let the server decide 67 + const priority = opts?.overridePriorityNotifications ? false : undefined 68 + 62 69 const query = useInfiniteQuery< 63 70 FeedPage, 64 71 Error, ··· 67 74 RQPageParam 68 75 >({ 69 76 staleTime: STALE.INFINITY, 70 - queryKey: RQKEY(), 77 + queryKey: RQKEY(priority), 71 78 async queryFn({pageParam}: {pageParam: RQPageParam}) { 72 79 let page 73 80 if (!pageParam) { ··· 75 82 page = unreads.getCachedUnreadPage() 76 83 } 77 84 if (!page) { 78 - page = ( 79 - await fetchPage({ 80 - agent, 81 - limit: PAGE_SIZE, 82 - cursor: pageParam, 83 - queryClient, 84 - moderationOpts, 85 - fetchAdditionalData: true, 86 - shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), 87 - }) 88 - ).page 85 + const {page: fetchedPage} = await fetchPage({ 86 + agent, 87 + limit: PAGE_SIZE, 88 + cursor: pageParam, 89 + queryClient, 90 + moderationOpts, 91 + fetchAdditionalData: true, 92 + shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'), 93 + priority, 94 + }) 95 + page = fetchedPage 89 96 } 90 97 91 98 // if the first page has an unread, mark all read
+67
src/state/queries/notifications/settings.ts
··· 1 + import {msg} from '@lingui/macro' 2 + import {useLingui} from '@lingui/react' 3 + import {useMutation, useQueryClient} from '@tanstack/react-query' 4 + 5 + import {until} from '#/lib/async/until' 6 + import {logger} from '#/logger' 7 + import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 8 + import {useAgent} from '#/state/session' 9 + import * as Toast from '#/view/com/util/Toast' 10 + 11 + export function useNotificationsSettingsMutation() { 12 + const {_} = useLingui() 13 + const agent = useAgent() 14 + const queryClient = useQueryClient() 15 + 16 + return useMutation({ 17 + mutationFn: async (keys: string[]) => { 18 + const enabled = keys[0] === 'enabled' 19 + 20 + await agent.api.app.bsky.notification.putPreferences({ 21 + priority: enabled, 22 + }) 23 + 24 + await until( 25 + 5, // 5 tries 26 + 1e3, // 1s delay between tries 27 + res => res.data.priority === enabled, 28 + () => agent.api.app.bsky.notification.listNotifications({limit: 1}), 29 + ) 30 + 31 + eagerlySetCachedPriority(queryClient, enabled) 32 + }, 33 + onError: err => { 34 + logger.error('Failed to save notification preferences', { 35 + safeMessage: err, 36 + }) 37 + Toast.show( 38 + _(msg`Failed to save notification preferences, please try again`), 39 + 'xmark', 40 + ) 41 + }, 42 + onSuccess: () => { 43 + Toast.show(_(msg`Preference saved`)) 44 + }, 45 + onSettled: () => { 46 + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) 47 + }, 48 + }) 49 + } 50 + 51 + function eagerlySetCachedPriority( 52 + queryClient: ReturnType<typeof useQueryClient>, 53 + enabled: boolean, 54 + ) { 55 + queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => { 56 + if (!old) return old 57 + return { 58 + ...old, 59 + pages: old.pages.map((page: any) => { 60 + return { 61 + ...page, 62 + priority: enabled, 63 + } 64 + }), 65 + } 66 + }) 67 + }
+1
src/state/queries/notifications/types.ts
··· 22 22 cursor: string | undefined 23 23 seenAt: Date 24 24 items: FeedNotification[] 25 + priority: boolean 25 26 } 26 27 27 28 export interface CachedFeedPage {
+7 -1
src/state/queries/notifications/util.ts
··· 39 39 moderationOpts: ModerationOpts | undefined 40 40 fetchAdditionalData: boolean 41 41 shouldUngroupFollowBacks?: () => boolean 42 - }): Promise<{page: FeedPage; indexedAt: string | undefined}> { 42 + priority?: boolean 43 + }): Promise<{ 44 + page: FeedPage 45 + indexedAt: string | undefined 46 + }> { 43 47 const res = await agent.listNotifications({ 44 48 limit, 45 49 cursor, 50 + // priority, 46 51 }) 47 52 48 53 const indexedAt = res.data.notifications[0]?.indexedAt ··· 88 93 cursor: res.data.cursor, 89 94 seenAt, 90 95 items: notifsGrouped, 96 + priority: res.data.priority ?? false, 91 97 }, 92 98 indexedAt, 93 99 }
+6 -1
src/view/com/notifications/Feed.tsx
··· 35 35 onPressTryAgain, 36 36 onScrolledDownChange, 37 37 ListHeaderComponent, 38 + overridePriorityNotifications, 38 39 }: { 39 40 scrollElRef?: ListRef 40 41 onPressTryAgain?: () => void 41 42 onScrolledDownChange: (isScrolledDown: boolean) => void 42 43 ListHeaderComponent?: () => JSX.Element 44 + overridePriorityNotifications?: boolean 43 45 }) { 44 46 const initialNumToRender = useInitialNumToRender() 45 47 ··· 59 61 hasNextPage, 60 62 isFetchingNextPage, 61 63 fetchNextPage, 62 - } = useNotificationFeedQuery({enabled: !!moderationOpts}) 64 + } = useNotificationFeedQuery({ 65 + enabled: !!moderationOpts, 66 + overridePriorityNotifications, 67 + }) 63 68 const isEmpty = !isFetching && !data?.pages[0]?.items.length 64 69 65 70 const items = React.useMemo(() => {
+76 -41
src/view/screens/Notifications.tsx
··· 1 - import React from 'react' 1 + import React, {useCallback} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {useFocusEffect, useIsFocused} from '@react-navigation/native' 6 6 import {useQueryClient} from '@tanstack/react-query' 7 7 8 + import {useAnalytics} from '#/lib/analytics/analytics' 8 9 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 10 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 11 + import {ComposeIcon2} from '#/lib/icons' 12 + import { 13 + NativeStackScreenProps, 14 + NotificationsTabNavigatorParams, 15 + } from '#/lib/routes/types' 16 + import {s} from '#/lib/styles' 9 17 import {logger} from '#/logger' 10 18 import {isNative} from '#/platform/detection' 11 19 import {emitSoftReset, listenSoftReset} from '#/state/events' ··· 17 25 import {truncateAndInvalidate} from '#/state/queries/util' 18 26 import {useSetMinimalShellMode} from '#/state/shell' 19 27 import {useComposerControls} from '#/state/shell/composer' 20 - import {useAnalytics} from 'lib/analytics/analytics' 21 - import {usePalette} from 'lib/hooks/usePalette' 22 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 23 - import {ComposeIcon2} from 'lib/icons' 24 - import { 25 - NativeStackScreenProps, 26 - NotificationsTabNavigatorParams, 27 - } from 'lib/routes/types' 28 - import {colors, s} from 'lib/styles' 29 - import {TextLink} from 'view/com/util/Link' 28 + import {Feed} from '#/view/com/notifications/Feed' 29 + import {FAB} from '#/view/com/util/fab/FAB' 30 + import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' 31 + import {ViewHeader} from '#/view/com/util/ViewHeader' 30 32 import {ListMethods} from 'view/com/util/List' 31 33 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 32 34 import {CenteredView} from 'view/com/util/Views' 35 + import {atoms as a, useTheme} from '#/alf' 36 + import {Button} from '#/components/Button' 37 + import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' 38 + import {Link} from '#/components/Link' 33 39 import {Loader} from '#/components/Loader' 34 - import {Feed} from '../com/notifications/Feed' 35 - import {FAB} from '../com/util/fab/FAB' 36 - import {MainScrollProvider} from '../com/util/MainScrollProvider' 37 - import {ViewHeader} from '../com/util/ViewHeader' 40 + import {Text} from '#/components/Typography' 38 41 39 42 type Props = NativeStackScreenProps< 40 43 NotificationsTabNavigatorParams, 41 44 'Notifications' 42 45 > 43 - export function NotificationsScreen({}: Props) { 46 + export function NotificationsScreen({route: {params}}: Props) { 44 47 const {_} = useLingui() 45 48 const setMinimalShellMode = useSetMinimalShellMode() 46 49 const [isScrolledDown, setIsScrolledDown] = React.useState(false) 47 50 const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) 48 51 const scrollElRef = React.useRef<ListMethods>(null) 49 52 const {screen} = useAnalytics() 50 - const pal = usePalette('default') 53 + const t = useTheme() 51 54 const {isDesktop} = useWebMediaQueries() 52 55 const queryClient = useQueryClient() 53 56 const unreadNotifs = useUnreadNotifications() ··· 109 112 return listenSoftReset(onPressLoadLatest) 110 113 }, [onPressLoadLatest, isScreenFocused]) 111 114 115 + const renderButton = useCallback(() => { 116 + return ( 117 + <Link 118 + to="/notifications/settings" 119 + label={_(msg`Notification settings`)} 120 + size="small" 121 + variant="ghost" 122 + color="secondary" 123 + shape="square" 124 + style={[a.justify_center]}> 125 + <SettingsIcon size="md" style={t.atoms.text_contrast_medium} /> 126 + </Link> 127 + ) 128 + }, [_, t]) 129 + 112 130 const ListHeaderComponent = React.useCallback(() => { 113 131 if (isDesktop) { 114 132 return ( 115 133 <View 116 134 style={[ 117 - pal.view, 118 - { 119 - flexDirection: 'row', 120 - alignItems: 'center', 121 - justifyContent: 'space-between', 122 - paddingHorizontal: 18, 123 - paddingVertical: 12, 124 - }, 135 + t.atoms.bg, 136 + a.flex_row, 137 + a.align_center, 138 + a.justify_between, 139 + a.gap_lg, 140 + a.px_lg, 141 + a.pr_md, 142 + a.py_sm, 125 143 ]}> 126 - <TextLink 127 - type="title-lg" 128 - href="/notifications" 129 - style={[pal.text, {fontWeight: 'bold'}]} 130 - text={ 131 - <> 132 - <Trans>Notifications</Trans>{' '} 144 + <Button 145 + label={_(msg`Notifications`)} 146 + accessibilityHint={_(msg`Refresh notifications`)} 147 + onPress={emitSoftReset}> 148 + {({hovered, pressed}) => ( 149 + <Text 150 + style={[ 151 + a.text_2xl, 152 + a.font_bold, 153 + (hovered || pressed) && a.underline, 154 + ]}> 155 + <Trans>Notifications</Trans> 133 156 {hasNew && ( 134 157 <View 135 158 style={{ 159 + left: 4, 136 160 top: -8, 137 - backgroundColor: colors.blue3, 161 + backgroundColor: t.palette.primary_500, 138 162 width: 8, 139 163 height: 8, 140 164 borderRadius: 4, 141 165 }} 142 166 /> 143 167 )} 144 - </> 145 - } 146 - onPress={emitSoftReset} 147 - /> 148 - {isLoadingLatest ? <Loader size="md" /> : <></>} 168 + </Text> 169 + )} 170 + </Button> 171 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 172 + {isLoadingLatest ? <Loader size="md" /> : <></>} 173 + {renderButton()} 174 + </View> 149 175 </View> 150 176 ) 151 177 } 152 178 return <></> 153 - }, [isDesktop, pal, hasNew, isLoadingLatest]) 179 + }, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest]) 154 180 155 181 const renderHeaderSpinner = React.useCallback(() => { 156 182 return ( 157 - <View style={{width: 30, height: 20, alignItems: 'flex-end'}}> 183 + <View 184 + style={[ 185 + {width: 30, height: 20}, 186 + a.flex_row, 187 + a.align_center, 188 + a.justify_end, 189 + a.gap_md, 190 + ]}> 158 191 {isLoadingLatest ? <Loader width={20} /> : <></>} 192 + {renderButton()} 159 193 </View> 160 194 ) 161 - }, [isLoadingLatest]) 195 + }, [renderButton, isLoadingLatest]) 162 196 163 197 return ( 164 198 <CenteredView ··· 176 210 onScrolledDownChange={setIsScrolledDown} 177 211 scrollElRef={scrollElRef} 178 212 ListHeaderComponent={ListHeaderComponent} 213 + overridePriorityNotifications={params?.show === 'all'} 179 214 /> 180 215 </MainScrollProvider> 181 216 {(isScrolledDown || hasNew) && (
+94
src/view/screens/NotificationsSettings.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 8 + import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' 9 + import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings' 10 + import {ViewHeader} from '#/view/com/util/ViewHeader' 11 + import {CenteredView} from '#/view/com/util/Views' 12 + import {atoms as a, useTheme} from '#/alf' 13 + import {Error} from '#/components/Error' 14 + import * as Toggle from '#/components/forms/Toggle' 15 + import {Loader} from '#/components/Loader' 16 + import {Text} from '#/components/Typography' 17 + 18 + type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationsSettings'> 19 + export function NotificationsSettingsScreen({}: Props) { 20 + const {_} = useLingui() 21 + const t = useTheme() 22 + 23 + const {data, isError: isQueryError, refetch} = useNotificationFeedQuery() 24 + const serverPriority = data?.pages.at(0)?.priority 25 + 26 + const { 27 + mutate: onChangePriority, 28 + isPending: isMutationPending, 29 + variables, 30 + } = useNotificationsSettingsMutation() 31 + 32 + const priority = isMutationPending 33 + ? variables[0] === 'enabled' 34 + : serverPriority 35 + 36 + return ( 37 + <CenteredView style={a.flex_1} sideBorders> 38 + <ViewHeader 39 + title={_(msg`Notification Settings`)} 40 + showOnDesktop 41 + showBorder 42 + /> 43 + {isQueryError ? ( 44 + <Error 45 + title={_(msg`Oops!`)} 46 + message={_(msg`Something went wrong!`)} 47 + onRetry={refetch} 48 + sideBorders={false} 49 + /> 50 + ) : ( 51 + <View style={[a.p_lg, a.gap_md]}> 52 + <Text style={[a.text_lg, a.font_bold]}> 53 + <FontAwesomeIcon icon="flask" style={t.atoms.text} />{' '} 54 + <Trans>Notification filters</Trans> 55 + </Text> 56 + <Toggle.Group 57 + label={_(msg`Priority notifications`)} 58 + type="checkbox" 59 + values={priority ? ['enabled'] : []} 60 + onChange={onChangePriority} 61 + disabled={typeof priority !== 'boolean' || isMutationPending}> 62 + <View> 63 + <Toggle.Item 64 + name="enabled" 65 + label={_(msg`Enable priority notifications`)} 66 + style={[a.justify_between, a.py_sm]}> 67 + <Toggle.LabelText> 68 + <Trans>Enable priority notifications</Trans> 69 + </Toggle.LabelText> 70 + {!data ? <Loader size="md" /> : <Toggle.Platform />} 71 + </Toggle.Item> 72 + </View> 73 + </Toggle.Group> 74 + <View 75 + style={[ 76 + a.mt_sm, 77 + a.px_xl, 78 + a.py_lg, 79 + a.rounded_md, 80 + t.atoms.bg_contrast_25, 81 + ]}> 82 + <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> 83 + <Trans> 84 + Experimental: When this preference is enabled, you'll only 85 + receive reply and quote notifications from users you follow. 86 + We'll continue to add more controls here over time. 87 + </Trans> 88 + </Text> 89 + </View> 90 + </View> 91 + )} 92 + </CenteredView> 93 + ) 94 + }
+4 -4
yarn.lock
··· 34 34 jsonpointer "^5.0.0" 35 35 leven "^3.1.0" 36 36 37 - "@atproto/api@^0.12.23": 38 - version "0.12.23" 39 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af" 40 - integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA== 37 + "@atproto/api@0.12.25": 38 + version "0.12.25" 39 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b" 40 + integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A== 41 41 dependencies: 42 42 "@atproto/common-web" "^0.3.0" 43 43 "@atproto/lexicon" "^0.4.0"