Skip to content

Commit

Permalink
Priority notifications (#4798)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Hailey <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent 9bd8393 commit cfb8a31
Show file tree
Hide file tree
Showing 20 changed files with 305 additions and 84 deletions.
1 change: 1 addition & 0 deletions bskyweb/cmd/bskyweb/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Lists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions src/components/forms/Toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -459,3 +460,5 @@ export function Radio() {
</View>
)
}

export const Platform = isNative ? Switch : Checkbox
5 changes: 0 additions & 5 deletions src/components/icons/Gear.tsx

This file was deleted.

10 changes: 6 additions & 4 deletions src/lib/async/until.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
}
Expand Down
15 changes: 6 additions & 9 deletions src/lib/routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand All @@ -69,7 +68,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
}

export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
Notifications: {show?: 'all'}
}

export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
Expand All @@ -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'}
}
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/screens/Messages/Conversation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Messages/List/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]}>
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Messages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 22 additions & 15 deletions src/state/queries/notifications/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -67,25 +74,25 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
RQPageParam
>({
staleTime: STALE.INFINITY,
queryKey: RQKEY(),
queryKey: RQKEY(priority),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
let page
if (!pageParam) {
// for the first page, we check the cached page held by the unread-checker first
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
Expand Down
67 changes: 67 additions & 0 deletions src/state/queries/notifications/settings.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}),
}
})
}
1 change: 1 addition & 0 deletions src/state/queries/notifications/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface FeedPage {
cursor: string | undefined
seenAt: Date
items: FeedNotification[]
priority: boolean
}

export interface CachedFeedPage {
Expand Down
8 changes: 7 additions & 1 deletion src/state/queries/notifications/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +93,7 @@ export async function fetchPage({
cursor: res.data.cursor,
seenAt,
items: notifsGrouped,
priority: res.data.priority ?? false,
},
indexedAt,
}
Expand Down
7 changes: 6 additions & 1 deletion src/view/com/notifications/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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(() => {
Expand Down
Loading

0 comments on commit cfb8a31

Please sign in to comment.