import {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 ( ) } 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, ) => { 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, ) => { 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 = 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 ( Keep me posted Get notified of this account’s activity {includeProfile && ( )} Posts Replies {error && ( Could not save changes: {cleanError(error)} )} ) } function parseActivitySubscription( sub?: AppBskyNotificationDefs.ActivitySubscription, ): Un$Typed { if (!sub) return {post: false, reply: false} const {post, reply} = sub return {post, reply} }