integrate api

This commit is contained in:
Samuel Newman
2025-06-27 15:22:14 +03:00
parent aef1b1971e
commit 2902ab9b48
@@ -1,19 +1,32 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type ModerationOpts} from '@atproto/api' import {type AppBskyNotificationDefs, type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {sanitizeHandle} from 'bskyogcard/dist/util/sanitizeHandle'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {platform, useTheme, web} from '#/alf' import {platform, useTheme, web} from '#/alf'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, type ButtonProps, ButtonText} from '#/components/Button' import {
Button,
ButtonIcon,
type ButtonProps,
ButtonText,
} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {Admonition} from '../Admonition'
export function SubscribeProfileDialog({ export function SubscribeProfileDialog({
control, control,
@@ -38,11 +51,10 @@ export function SubscribeProfileDialog({
) )
} }
// TEMP - should be derived from profile view const defaultState = {
const initialState = { post: false,
posts: false, reply: false,
replies: false, } satisfies AppBskyNotificationDefs.ActivitySubscription
}
function DialogInner({ function DialogInner({
profile, profile,
@@ -55,52 +67,93 @@ function DialogInner({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const agent = useAgent()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const initialState = profile.viewer?.activitySubscription || defaultState
const [state, setState] = useState(initialState) const [state, setState] = useState(initialState)
const values = useMemo(() => { const values = useMemo(() => {
const {posts, replies} = state const {post, reply} = state
const res = [] const res = []
if (posts) res.push('posts') if (post) res.push('post')
if (replies) res.push('replies') if (reply) res.push('reply')
return res return res
}, [state]) }, [state])
const onChange = (newValues: string[]) => { const onChange = (newValues: string[]) => {
setState(oldValues => { setState(oldValues => {
// ensure you can't have replies without posts // ensure you can't have reply without post
if (!oldValues.replies && newValues.includes('replies')) { if (!oldValues.reply && newValues.includes('reply')) {
return { return {
posts: true, post: true,
replies: true, reply: true,
} }
} }
if (oldValues.posts && !newValues.includes('posts')) { if (oldValues.post && !newValues.includes('post')) {
return { return {
posts: false, post: false,
replies: false, reply: false,
} }
} }
return { return {
posts: newValues.includes('posts'), post: newValues.includes('post'),
replies: newValues.includes('replies'), reply: newValues.includes('reply'),
} }
}) })
} }
const {
mutate: saveChanges,
isPending: isSaving,
error,
} = useMutation({
mutationFn: async (
activitySubscription: AppBskyNotificationDefs.ActivitySubscription,
) => {
await new Promise(resolve => setTimeout(resolve, 1000))
await agent.app.bsky.notification.putActivitySubscription({
subject: profile.did,
activitySubscription,
})
},
onSuccess: (_data, variables) => {
control.close()
if (!variables.post && !variables.reply) {
Toast.show(
_(
msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
),
'check',
)
} else if (!initialState.post && !initialState) {
Toast.show(
_(
msg`You'll start receiving notifications for ${sanitizeHandle(profile.handle, '@')}!`,
),
'check',
)
} else {
Toast.show(_(msg`Changes saved`), 'check')
}
},
onError: error => {
logger.error('Could not save activity subscription', {message: error})
},
})
const buttonProps: Omit<ButtonProps, 'children'> = useMemo(() => { const buttonProps: Omit<ButtonProps, 'children'> = useMemo(() => {
const isDirty = const isDirty =
state.posts !== initialState.posts || state.post !== initialState.post || state.reply !== initialState.reply
state.replies !== initialState.replies const hasAny = state.post || state.reply
const hasAny = state.posts || state.replies
if (isDirty) { if (isDirty) {
return { return {
label: _(msg`Save changes`), label: _(msg`Save changes`),
color: hasAny ? 'primary' : 'negative', color: hasAny ? 'primary' : 'negative',
onPress: () => saveChanges(state),
disabled: isSaving,
} }
} else { } else {
// on web, a disabled save button feels more natural than a massive close button // on web, a disabled save button feels more natural than a massive close button
@@ -114,10 +167,11 @@ function DialogInner({
return { return {
label: _(msg`Cancel`), label: _(msg`Cancel`),
color: 'secondary', color: 'secondary',
onPress: () => control.close(),
} }
} }
} }
}, [state, _]) }, [state, initialState, control, _, isSaving, saveChanges])
const name = createSanitizedDisplayName(profile, false) const name = createSanitizedDisplayName(profile, false)
@@ -156,7 +210,7 @@ function DialogInner({
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<Toggle.Item <Toggle.Item
label={_(msg`Posts`)} label={_(msg`Posts`)}
name="posts" name="post"
style={[ style={[
a.flex_1, a.flex_1,
a.py_xs, a.py_xs,
@@ -173,7 +227,7 @@ function DialogInner({
</Toggle.Item> </Toggle.Item>
<Toggle.Item <Toggle.Item
label={_(msg`Replies`)} label={_(msg`Replies`)}
name="replies" name="reply"
style={[ style={[
a.flex_1, a.flex_1,
a.py_xs, a.py_xs,
@@ -191,12 +245,15 @@ function DialogInner({
</View> </View>
</Toggle.Group> </Toggle.Group>
<Button {error && (
{...buttonProps} <Admonition type="error">
size="large" <Trans>Could not save changes: {cleanError(error)}</Trans>
variant="solid" </Admonition>
onPress={() => control.close()}> )}
<Button {...buttonProps} size="large" variant="solid">
<ButtonText>{buttonProps.label}</ButtonText> <ButtonText>{buttonProps.label}</ButtonText>
{isSaving && <ButtonIcon icon={Loader} />}
</Button> </Button>
</View> </View>