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>zio/stable
parent
9bd8393685
commit
cfb8a3160e
|
@ -197,6 +197,7 @@ func serve(cctx *cli.Context) error {
|
|||
e.GET("/search", server.WebGeneric)
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
e.GET("/notifications/settings", server.WebGeneric)
|
||||
e.GET("/lists", server.WebGeneric)
|
||||
e.GET("/moderation", server.WebGeneric)
|
||||
e.GET("/moderation/modlists", server.WebGeneric)
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.12.23",
|
||||
"@atproto/api": "0.12.25",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
|
|
|
@ -76,6 +76,7 @@ import {LogScreen} from './view/screens/Log'
|
|||
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
|
||||
import {NotFoundScreen} from './view/screens/NotFound'
|
||||
import {NotificationsScreen} from './view/screens/Notifications'
|
||||
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
|
||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||
import {PostThreadScreen} from './view/screens/PostThread'
|
||||
|
@ -324,6 +325,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||
getComponent={() => MessagesSettingsScreen}
|
||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationsSettings"
|
||||
getComponent={() => NotificationsSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Feeds"
|
||||
getComponent={() => FeedsScreen}
|
||||
|
|
|
@ -189,7 +189,7 @@ let ListMaybePlaceholder = ({
|
|||
return (
|
||||
<Error
|
||||
title={errorTitle ?? _(msg`Oops!`)}
|
||||
message={errorMessage ?? _(`Something went wrong!`)}
|
||||
message={errorMessage ?? _(msg`Something went wrong!`)}
|
||||
onRetry={onRetry}
|
||||
onGoBack={onGoBack}
|
||||
sideBorders={sideBorders}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
|||
import {Pressable, View, ViewStyle} from 'react-native'
|
||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {
|
||||
atoms as a,
|
||||
|
@ -459,3 +460,5 @@ export function Radio() {
|
|||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const Platform = isNative ? Switch : Checkbox
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const SettingsGear2_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
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',
|
||||
})
|
|
@ -1,10 +1,10 @@
|
|||
import {timeout} from './timeout'
|
||||
|
||||
export async function until(
|
||||
export async function until<T>(
|
||||
retries: number,
|
||||
delay: number,
|
||||
cond: (v: any, err: any) => boolean,
|
||||
fn: () => Promise<any>,
|
||||
cond: (v: T, err: any) => boolean,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<boolean> {
|
||||
while (retries > 0) {
|
||||
try {
|
||||
|
@ -13,7 +13,9 @@ export async function until(
|
|||
return true
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (cond(undefined, e)) {
|
||||
// TODO: change the type signature of cond to accept undefined
|
||||
// however this breaks every existing usage of until -sfn
|
||||
if (cond(undefined as unknown as T, e)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,14 +42,13 @@ export type CommonNavigatorParams = {
|
|||
Hashtag: {tag: string; author?: string}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
MessagesSettings: undefined
|
||||
NotificationsSettings: undefined
|
||||
Feeds: undefined
|
||||
Start: {name: string; rkey: string}
|
||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {
|
||||
rkey?: string
|
||||
}
|
||||
StarterPackEdit: {rkey?: string}
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
|
|||
}
|
||||
|
||||
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
}
|
||||
|
||||
export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
@ -84,7 +83,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
|||
Home: undefined
|
||||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||
}
|
||||
|
@ -96,7 +95,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
Notifications: {show?: 'all'}
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
MessagesTab: undefined
|
||||
|
@ -105,9 +104,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||
StarterPackShort: {code: string}
|
||||
StarterPackWizard: undefined
|
||||
StarterPackEdit: {
|
||||
rkey?: string
|
||||
}
|
||||
StarterPackEdit: {rkey?: string}
|
||||
}
|
||||
|
||||
// NOTE
|
||||
|
|
|
@ -5,6 +5,7 @@ export const router = new Router({
|
|||
Search: '/search',
|
||||
Feeds: '/feeds',
|
||||
Notifications: '/notifications',
|
||||
NotificationsSettings: '/notifications/settings',
|
||||
Settings: '/settings',
|
||||
LanguageSettings: '/settings/language',
|
||||
Lists: '/lists',
|
||||
|
|
|
@ -106,6 +106,7 @@ function Inner() {
|
|||
title={_(msg`Something went wrong`)}
|
||||
message={_(msg`We couldn't load this conversation`)}
|
||||
onRetry={() => convoState.error.retry()}
|
||||
sideBorders={false}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
|
|
|
@ -309,7 +309,7 @@ function DesktopHeader({
|
|||
a.gap_lg,
|
||||
a.px_lg,
|
||||
a.pr_md,
|
||||
a.py_md,
|
||||
a.py_sm,
|
||||
a.border_b,
|
||||
t.atoms.border_contrast_low,
|
||||
]}>
|
||||
|
|
|
@ -107,7 +107,7 @@ export function MessagesSettingsScreen({}: Props) {
|
|||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[t.atoms.text_contrast_high]}>
|
||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||
<Trans>
|
||||
You can continue ongoing conversations regardless of which setting
|
||||
you choose.
|
||||
|
|
|
@ -46,11 +46,14 @@ const PAGE_SIZE = 30
|
|||
type RQPageParam = string | undefined
|
||||
|
||||
const RQKEY_ROOT = 'notification-feed'
|
||||
export function RQKEY() {
|
||||
return [RQKEY_ROOT]
|
||||
export function RQKEY(priority?: false) {
|
||||
return [RQKEY_ROOT, priority]
|
||||
}
|
||||
|
||||
export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
||||
export function useNotificationFeedQuery(opts?: {
|
||||
enabled?: boolean
|
||||
overridePriorityNotifications?: boolean
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
@ -59,6 +62,10 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||
const lastPageCountRef = useRef(0)
|
||||
const gate = useGate()
|
||||
|
||||
// false: force showing all notifications
|
||||
// undefined: let the server decide
|
||||
const priority = opts?.overridePriorityNotifications ? false : undefined
|
||||
|
||||
const query = useInfiniteQuery<
|
||||
FeedPage,
|
||||
Error,
|
||||
|
@ -67,7 +74,7 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||
RQPageParam
|
||||
>({
|
||||
staleTime: STALE.INFINITY,
|
||||
queryKey: RQKEY(),
|
||||
queryKey: RQKEY(priority),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
let page
|
||||
if (!pageParam) {
|
||||
|
@ -75,17 +82,17 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
|
|||
page = unreads.getCachedUnreadPage()
|
||||
}
|
||||
if (!page) {
|
||||
page = (
|
||||
await fetchPage({
|
||||
agent,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
fetchAdditionalData: true,
|
||||
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
|
||||
})
|
||||
).page
|
||||
const {page: fetchedPage} = await fetchPage({
|
||||
agent,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
queryClient,
|
||||
moderationOpts,
|
||||
fetchAdditionalData: true,
|
||||
shouldUngroupFollowBacks: () => gate('ungroup_follow_backs'),
|
||||
priority,
|
||||
})
|
||||
page = fetchedPage
|
||||
}
|
||||
|
||||
// if the first page has an unread, mark all read
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {until} from '#/lib/async/until'
|
||||
import {logger} from '#/logger'
|
||||
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
|
||||
export function useNotificationsSettingsMutation() {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (keys: string[]) => {
|
||||
const enabled = keys[0] === 'enabled'
|
||||
|
||||
await agent.api.app.bsky.notification.putPreferences({
|
||||
priority: enabled,
|
||||
})
|
||||
|
||||
await until(
|
||||
5, // 5 tries
|
||||
1e3, // 1s delay between tries
|
||||
res => res.data.priority === enabled,
|
||||
() => agent.api.app.bsky.notification.listNotifications({limit: 1}),
|
||||
)
|
||||
|
||||
eagerlySetCachedPriority(queryClient, enabled)
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to save notification preferences', {
|
||||
safeMessage: err,
|
||||
})
|
||||
Toast.show(
|
||||
_(msg`Failed to save notification preferences, please try again`),
|
||||
'xmark',
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Preference saved`))
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function eagerlySetCachedPriority(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
enabled: boolean,
|
||||
) {
|
||||
queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page: any) => {
|
||||
return {
|
||||
...page,
|
||||
priority: enabled,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
|
@ -22,6 +22,7 @@ export interface FeedPage {
|
|||
cursor: string | undefined
|
||||
seenAt: Date
|
||||
items: FeedNotification[]
|
||||
priority: boolean
|
||||
}
|
||||
|
||||
export interface CachedFeedPage {
|
||||
|
|
|
@ -39,10 +39,15 @@ export async function fetchPage({
|
|||
moderationOpts: ModerationOpts | undefined
|
||||
fetchAdditionalData: boolean
|
||||
shouldUngroupFollowBacks?: () => boolean
|
||||
}): Promise<{page: FeedPage; indexedAt: string | undefined}> {
|
||||
priority?: boolean
|
||||
}): Promise<{
|
||||
page: FeedPage
|
||||
indexedAt: string | undefined
|
||||
}> {
|
||||
const res = await agent.listNotifications({
|
||||
limit,
|
||||
cursor,
|
||||
// priority,
|
||||
})
|
||||
|
||||
const indexedAt = res.data.notifications[0]?.indexedAt
|
||||
|
@ -88,6 +93,7 @@ export async function fetchPage({
|
|||
cursor: res.data.cursor,
|
||||
seenAt,
|
||||
items: notifsGrouped,
|
||||
priority: res.data.priority ?? false,
|
||||
},
|
||||
indexedAt,
|
||||
}
|
||||
|
|
|
@ -35,11 +35,13 @@ export function Feed({
|
|||
onPressTryAgain,
|
||||
onScrolledDownChange,
|
||||
ListHeaderComponent,
|
||||
overridePriorityNotifications,
|
||||
}: {
|
||||
scrollElRef?: ListRef
|
||||
onPressTryAgain?: () => void
|
||||
onScrolledDownChange: (isScrolledDown: boolean) => void
|
||||
ListHeaderComponent?: () => JSX.Element
|
||||
overridePriorityNotifications?: boolean
|
||||
}) {
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
|
||||
|
@ -59,7 +61,10 @@ export function Feed({
|
|||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
} = useNotificationFeedQuery({enabled: !!moderationOpts})
|
||||
} = useNotificationFeedQuery({
|
||||
enabled: !!moderationOpts,
|
||||
overridePriorityNotifications,
|
||||
})
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.items.length
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import React from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
NotificationsTabNavigatorParams,
|
||||
} from '#/lib/routes/types'
|
||||
import {s} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {emitSoftReset, listenSoftReset} from '#/state/events'
|
||||
|
@ -17,37 +25,32 @@ import {
|
|||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {
|
||||
NativeStackScreenProps,
|
||||
NotificationsTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
import {Feed} from '#/view/com/notifications/Feed'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {ListMethods} from 'view/com/util/List'
|
||||
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Feed} from '../com/notifications/Feed'
|
||||
import {FAB} from '../com/util/fab/FAB'
|
||||
import {MainScrollProvider} from '../com/util/MainScrollProvider'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
NotificationsTabNavigatorParams,
|
||||
'Notifications'
|
||||
>
|
||||
export function NotificationsScreen({}: Props) {
|
||||
export function NotificationsScreen({route: {params}}: Props) {
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
|
||||
const scrollElRef = React.useRef<ListMethods>(null)
|
||||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const queryClient = useQueryClient()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
|
@ -109,56 +112,87 @@ export function NotificationsScreen({}: Props) {
|
|||
return listenSoftReset(onPressLoadLatest)
|
||||
}, [onPressLoadLatest, isScreenFocused])
|
||||
|
||||
const renderButton = useCallback(() => {
|
||||
return (
|
||||
<Link
|
||||
to="/notifications/settings"
|
||||
label={_(msg`Notification settings`)}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
shape="square"
|
||||
style={[a.justify_center]}>
|
||||
<SettingsIcon size="md" style={t.atoms.text_contrast_medium} />
|
||||
</Link>
|
||||
)
|
||||
}, [_, t])
|
||||
|
||||
const ListHeaderComponent = React.useCallback(() => {
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_between,
|
||||
a.gap_lg,
|
||||
a.px_lg,
|
||||
a.pr_md,
|
||||
a.py_sm,
|
||||
]}>
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
href="/notifications"
|
||||
style={[pal.text, {fontWeight: 'bold'}]}
|
||||
text={
|
||||
<>
|
||||
<Trans>Notifications</Trans>{' '}
|
||||
<Button
|
||||
label={_(msg`Notifications`)}
|
||||
accessibilityHint={_(msg`Refresh notifications`)}
|
||||
onPress={emitSoftReset}>
|
||||
{({hovered, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
a.text_2xl,
|
||||
a.font_bold,
|
||||
(hovered || pressed) && a.underline,
|
||||
]}>
|
||||
<Trans>Notifications</Trans>
|
||||
{hasNew && (
|
||||
<View
|
||||
style={{
|
||||
left: 4,
|
||||
top: -8,
|
||||
backgroundColor: colors.blue3,
|
||||
backgroundColor: t.palette.primary_500,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onPress={emitSoftReset}
|
||||
/>
|
||||
{isLoadingLatest ? <Loader size="md" /> : <></>}
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{isLoadingLatest ? <Loader size="md" /> : <></>}
|
||||
{renderButton()}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}, [isDesktop, pal, hasNew, isLoadingLatest])
|
||||
}, [isDesktop, t, hasNew, renderButton, _, isLoadingLatest])
|
||||
|
||||
const renderHeaderSpinner = React.useCallback(() => {
|
||||
return (
|
||||
<View style={{width: 30, height: 20, alignItems: 'flex-end'}}>
|
||||
<View
|
||||
style={[
|
||||
{width: 30, height: 20},
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.justify_end,
|
||||
a.gap_md,
|
||||
]}>
|
||||
{isLoadingLatest ? <Loader width={20} /> : <></>}
|
||||
{renderButton()}
|
||||
</View>
|
||||
)
|
||||
}, [isLoadingLatest])
|
||||
}, [renderButton, isLoadingLatest])
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
|
@ -176,6 +210,7 @@ export function NotificationsScreen({}: Props) {
|
|||
onScrolledDownChange={setIsScrolledDown}
|
||||
scrollElRef={scrollElRef}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
overridePriorityNotifications={params?.show === 'all'}
|
||||
/>
|
||||
</MainScrollProvider>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||
import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {CenteredView} from '#/view/com/util/Views'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Error} from '#/components/Error'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationsSettings'>
|
||||
export function NotificationsSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
|
||||
const serverPriority = data?.pages.at(0)?.priority
|
||||
|
||||
const {
|
||||
mutate: onChangePriority,
|
||||
isPending: isMutationPending,
|
||||
variables,
|
||||
} = useNotificationsSettingsMutation()
|
||||
|
||||
const priority = isMutationPending
|
||||
? variables[0] === 'enabled'
|
||||
: serverPriority
|
||||
|
||||
return (
|
||||
<CenteredView style={a.flex_1} sideBorders>
|
||||
<ViewHeader
|
||||
title={_(msg`Notification Settings`)}
|
||||
showOnDesktop
|
||||
showBorder
|
||||
/>
|
||||
{isQueryError ? (
|
||||
<Error
|
||||
title={_(msg`Oops!`)}
|
||||
message={_(msg`Something went wrong!`)}
|
||||
onRetry={refetch}
|
||||
sideBorders={false}
|
||||
/>
|
||||
) : (
|
||||
<View style={[a.p_lg, a.gap_md]}>
|
||||
<Text style={[a.text_lg, a.font_bold]}>
|
||||
<FontAwesomeIcon icon="flask" style={t.atoms.text} />{' '}
|
||||
<Trans>Notification filters</Trans>
|
||||
</Text>
|
||||
<Toggle.Group
|
||||
label={_(msg`Priority notifications`)}
|
||||
type="checkbox"
|
||||
values={priority ? ['enabled'] : []}
|
||||
onChange={onChangePriority}
|
||||
disabled={typeof priority !== 'boolean' || isMutationPending}>
|
||||
<View>
|
||||
<Toggle.Item
|
||||
name="enabled"
|
||||
label={_(msg`Enable priority notifications`)}
|
||||
style={[a.justify_between, a.py_sm]}>
|
||||
<Toggle.LabelText>
|
||||
<Trans>Enable priority notifications</Trans>
|
||||
</Toggle.LabelText>
|
||||
{!data ? <Loader size="md" /> : <Toggle.Platform />}
|
||||
</Toggle.Item>
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
<View
|
||||
style={[
|
||||
a.mt_sm,
|
||||
a.px_xl,
|
||||
a.py_lg,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||
<Trans>
|
||||
Experimental: When this preference is enabled, you'll only
|
||||
receive reply and quote notifications from users you follow.
|
||||
We'll continue to add more controls here over time.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
|
@ -34,10 +34,10 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atproto/api@^0.12.23":
|
||||
version "0.12.23"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af"
|
||||
integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA==
|
||||
"@atproto/api@0.12.25":
|
||||
version "0.12.25"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.25.tgz#9eeb51484106a5e07f89f124e505674a3574f93b"
|
||||
integrity sha512-IV3vGPnDw9bmyP/JOd8YKbm8fOpRAgJpEUVnIZNVb/Vo8v+WOroOjrJxtzdHOcXTL9IEcTTyXSCc7yE7kwhN2A==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
|
|
Loading…
Reference in New Issue