Something went wrong. Try again.
An ATproto social media client -- with an independent Appview.
Something went wrong. Try again.
9.1 kB · 309 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310import {useMemo, useState} from 'react'import {View} from 'react-native'import { type AppBskyNotificationDefs, type AppBskyNotificationListActivitySubscriptions, type ModerationOpts, type Un$Typed,} from '@atproto/api'import {msg, Trans} from '@lingui/macro'import {useLingui} from '@lingui/react'import { type InfiniteData, useMutation, useQueryClient,} from '@tanstack/react-query'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'import {cleanError} from '#/lib/strings/errors'import {sanitizeHandle} from '#/lib/strings/handles'import {logger} from '#/logger'import {isWeb} from '#/platform/detection'import {updateProfileShadow} from '#/state/cache/profile-shadow'import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'import {useAgent} from '#/state/session'import * as Toast from '#/view/com/util/Toast'import {platform, useTheme, web} from '#/alf'import {atoms as a} from '#/alf'import {Admonition} from '#/components/Admonition'import { Button, ButtonIcon, type ButtonProps, ButtonText,} from '#/components/Button'import * as Dialog from '#/components/Dialog'import * as Toggle from '#/components/forms/Toggle'import {Loader} from '#/components/Loader'import * as ProfileCard from '#/components/ProfileCard'import {Text} from '#/components/Typography'import type * as bsky from '#/types/bsky'
export function SubscribeProfileDialog({ control, profile, moderationOpts, includeProfile,}: { control: Dialog.DialogControlProps profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts includeProfile?: boolean}) { return ( <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Handle /> <DialogInner profile={profile} moderationOpts={moderationOpts} includeProfile={includeProfile} /> </Dialog.Outer> )}
function DialogInner({ profile, moderationOpts, includeProfile,}: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts includeProfile?: boolean}) { const {_} = useLingui() const t = useTheme() const agent = useAgent() const control = Dialog.useDialogContext() const queryClient = useQueryClient() const initialState = parseActivitySubscription( profile.viewer?.activitySubscription, ) const [state, setState] = useState(initialState)
const values = useMemo(() => { const {post, reply} = state const res = [] if (post) res.push('post') if (reply) res.push('reply') return res }, [state])
const onChange = (newValues: string[]) => { setState(oldValues => { // ensure you can't have reply without post if (!oldValues.reply && newValues.includes('reply')) { return { post: true, reply: true, } }
if (oldValues.post && !newValues.includes('post')) { return { post: false, reply: false, } }
return { post: newValues.includes('post'), reply: newValues.includes('reply'), } }) }
const { mutate: saveChanges, isPending: isSaving, error, } = useMutation({ mutationFn: async ( activitySubscription: Un$Typed<AppBskyNotificationDefs.ActivitySubscription>, ) => { await agent.app.bsky.notification.putActivitySubscription({ subject: profile.did, activitySubscription, }) }, onSuccess: (_data, activitySubscription) => { control.close(() => { updateProfileShadow(queryClient, profile.did, { activitySubscription, })
if (!activitySubscription.post && !activitySubscription.reply) { logger.metric('activitySubscription:disable', {}) Toast.show( _( msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`, ), 'check', )
// filter out the subscription queryClient.setQueryData( RQKEY_getActivitySubscriptions, ( old?: InfiniteData<AppBskyNotificationListActivitySubscriptions.OutputSchema>, ) => { if (!old) return old return { ...old, pages: old.pages.map(page => ({ ...page, subscriptions: page.subscriptions.filter( item => item.did !== profile.did, ), })), } }, ) } else { logger.metric('activitySubscription:enable', { setting: activitySubscription.reply ? 'posts_and_replies' : 'posts', }) if (!initialState.post && !initialState.reply) { Toast.show( _( msg`You'll start receiving notifications for ${sanitizeHandle(profile.handle, '@')}!`, ), 'check', ) } else { Toast.show(_(msg`Changes saved`), 'check') } } }) }, onError: err => { logger.error('Could not save activity subscription', {message: err}) }, })
const buttonProps: Omit<ButtonProps, 'children'> = useMemo(() => { const isDirty = state.post !== initialState.post || state.reply !== initialState.reply const hasAny = state.post || state.reply
if (isDirty) { return { label: _(msg`Save changes`), color: hasAny ? 'primary' : 'negative', onPress: () => saveChanges(state), disabled: isSaving, } } else { // on web, a disabled save button feels more natural than a massive close button if (isWeb) { return { label: _(msg`Save changes`), color: 'secondary', disabled: true, } } else { return { label: _(msg`Cancel`), color: 'secondary', onPress: () => control.close(), } } } }, [state, initialState, control, _, isSaving, saveChanges])
const name = createSanitizedDisplayName(profile, false)
return ( <Dialog.ScrollableInner style={web({maxWidth: 400})} label={_(msg`Get notified of new posts from ${name}`)}> <View style={[a.gap_lg]}> <View style={[a.gap_xs]}> <Text style={[a.font_heavy, a.text_2xl]}> <Trans>Keep me posted</Trans> </Text> <Text style={[t.atoms.text_contrast_medium, a.text_md]}> <Trans>Get notified of this account’s activity</Trans> </Text> </View>
{includeProfile && ( <ProfileCard.Header> <ProfileCard.Avatar profile={profile} moderationOpts={moderationOpts} disabledPreview /> <ProfileCard.NameAndHandle profile={profile} moderationOpts={moderationOpts} /> </ProfileCard.Header> )}
<Toggle.Group label={_(msg`Subscribe to account activity`)} values={values} onChange={onChange}> <View style={[a.gap_sm]}> <Toggle.Item label={_(msg`Posts`)} name="post" style={[ a.flex_1, a.py_xs, platform({ native: [a.justify_between], web: [a.flex_row_reverse, a.gap_sm], }), ]}> <Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> <Trans>Posts</Trans> </Toggle.LabelText> <Toggle.Switch /> </Toggle.Item> <Toggle.Item label={_(msg`Replies`)} name="reply" style={[ a.flex_1, a.py_xs, platform({ native: [a.justify_between], web: [a.flex_row_reverse, a.gap_sm], }), ]}> <Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}> <Trans>Replies</Trans> </Toggle.LabelText> <Toggle.Switch /> </Toggle.Item> </View> </Toggle.Group>
{error && ( <Admonition type="error"> <Trans>Could not save changes: {cleanError(error)}</Trans> </Admonition> )}
<Button {...buttonProps} size="large" variant="solid"> <ButtonText>{buttonProps.label}</ButtonText> {isSaving && <ButtonIcon icon={Loader} />} </Button> </View>
<Dialog.Close /> </Dialog.ScrollableInner> )}
function parseActivitySubscription( sub?: AppBskyNotificationDefs.ActivitySubscription,): Un$Typed<AppBskyNotificationDefs.ActivitySubscription> { if (!sub) return {post: false, reply: false} const {post, reply} = sub return {post, reply}}