this repo has no description
1import {useMemo, useState} from 'react'
2import {View} from 'react-native'
3import {
4 type AppBskyNotificationDefs,
5 type AppBskyNotificationListActivitySubscriptions,
6 type ModerationOpts,
7 type Un$Typed,
8} from '@atproto/api'
9import {msg} from '@lingui/core/macro'
10import {useLingui} from '@lingui/react'
11import {Trans} from '@lingui/react/macro'
12import {
13 type InfiniteData,
14 useMutation,
15 useQueryClient,
16} from '@tanstack/react-query'
17
18import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
19import {cleanError} from '#/lib/strings/errors'
20import {sanitizeHandle} from '#/lib/strings/handles'
21import {updateProfileShadow} from '#/state/cache/profile-shadow'
22import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
23import {useAgent} from '#/state/session'
24import {atoms as a, platform, useTheme, web} from '#/alf'
25import {Admonition} from '#/components/Admonition'
26import {
27 Button,
28 ButtonIcon,
29 type ButtonProps,
30 ButtonText,
31} from '#/components/Button'
32import * as Dialog from '#/components/Dialog'
33import * as Toggle from '#/components/forms/Toggle'
34import {Loader} from '#/components/Loader'
35import * as ProfileCard from '#/components/ProfileCard'
36import * as Toast from '#/components/Toast'
37import {Text} from '#/components/Typography'
38import {useAnalytics} from '#/analytics'
39import {IS_WEB} from '#/env'
40import type * as bsky from '#/types/bsky'
41
42export function SubscribeProfileDialog({
43 control,
44 profile,
45 moderationOpts,
46 includeProfile,
47}: {
48 control: Dialog.DialogControlProps
49 profile: bsky.profile.AnyProfileView
50 moderationOpts: ModerationOpts
51 includeProfile?: boolean
52}) {
53 return (
54 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
55 <Dialog.Handle />
56 <DialogInner
57 profile={profile}
58 moderationOpts={moderationOpts}
59 includeProfile={includeProfile}
60 />
61 </Dialog.Outer>
62 )
63}
64
65function DialogInner({
66 profile,
67 moderationOpts,
68 includeProfile,
69}: {
70 profile: bsky.profile.AnyProfileView
71 moderationOpts: ModerationOpts
72 includeProfile?: boolean
73}) {
74 const ax = useAnalytics()
75 const {_} = useLingui()
76 const t = useTheme()
77 const agent = useAgent()
78 const control = Dialog.useDialogContext()
79 const queryClient = useQueryClient()
80 const initialState = parseActivitySubscription(
81 profile.viewer?.activitySubscription,
82 )
83 const [state, setState] = useState(initialState)
84
85 const values = useMemo(() => {
86 const {post, reply} = state
87 const res = []
88 if (post) res.push('post')
89 if (reply) res.push('reply')
90 return res
91 }, [state])
92
93 const onChange = (newValues: string[]) => {
94 setState(oldValues => {
95 // ensure you can't have reply without post
96 if (!oldValues.reply && newValues.includes('reply')) {
97 return {
98 post: true,
99 reply: true,
100 }
101 }
102
103 if (oldValues.post && !newValues.includes('post')) {
104 return {
105 post: false,
106 reply: false,
107 }
108 }
109
110 return {
111 post: newValues.includes('post'),
112 reply: newValues.includes('reply'),
113 }
114 })
115 }
116
117 const {
118 mutate: saveChanges,
119 isPending: isSaving,
120 error,
121 } = useMutation({
122 mutationFn: async (
123 activitySubscription: Un$Typed<AppBskyNotificationDefs.ActivitySubscription>,
124 ) => {
125 await agent.app.bsky.notification.putActivitySubscription({
126 subject: profile.did,
127 activitySubscription,
128 })
129 },
130 onSuccess: (_data, activitySubscription) => {
131 control.close(() => {
132 updateProfileShadow(queryClient, profile.did, {
133 activitySubscription,
134 })
135
136 if (!activitySubscription.post && !activitySubscription.reply) {
137 ax.metric('activitySubscription:disable', {})
138 Toast.show(
139 _(
140 msg`You will no longer receive notifications for ${sanitizeHandle(profile.handle, '@')}`,
141 ),
142 {
143 type: 'success',
144 },
145 )
146
147 // filter out the subscription
148 queryClient.setQueryData(
149 RQKEY_getActivitySubscriptions,
150 (
151 old?: InfiniteData<AppBskyNotificationListActivitySubscriptions.OutputSchema>,
152 ) => {
153 if (!old) return old
154 return {
155 ...old,
156 pages: old.pages.map(page => ({
157 ...page,
158 subscriptions: page.subscriptions.filter(
159 item => item.did !== profile.did,
160 ),
161 })),
162 }
163 },
164 )
165 } else {
166 ax.metric('activitySubscription:enable', {
167 setting: activitySubscription.reply ? 'posts_and_replies' : 'posts',
168 })
169 if (!initialState.post && !initialState.reply) {
170 Toast.show(
171 _(
172 msg`You'll start receiving notifications for ${sanitizeHandle(profile.handle, '@')}!`,
173 ),
174 {
175 type: 'success',
176 },
177 )
178 } else {
179 Toast.show(_(msg`Changes saved`), {
180 type: 'success',
181 })
182 }
183 }
184 })
185 },
186 onError: err => {
187 ax.logger.error('Could not save activity subscription', {message: err})
188 },
189 })
190
191 const buttonProps: Omit<ButtonProps, 'children'> = useMemo(() => {
192 const isDirty =
193 state.post !== initialState.post || state.reply !== initialState.reply
194 const hasAny = state.post || state.reply
195
196 if (isDirty) {
197 return {
198 label: _(msg`Save changes`),
199 color: hasAny ? 'primary' : 'negative',
200 onPress: () => saveChanges(state),
201 disabled: isSaving,
202 }
203 } else {
204 // on web, a disabled save button feels more natural than a massive close button
205 if (IS_WEB) {
206 return {
207 label: _(msg`Save changes`),
208 color: 'secondary',
209 disabled: true,
210 }
211 } else {
212 return {
213 label: _(msg`Cancel`),
214 color: 'secondary',
215 onPress: () => control.close(),
216 }
217 }
218 }
219 }, [state, initialState, control, _, isSaving, saveChanges])
220
221 const name = createSanitizedDisplayName(profile, false)
222
223 return (
224 <Dialog.ScrollableInner
225 style={web({maxWidth: 400})}
226 label={_(msg`Get notified of new posts from ${name}`)}>
227 <View style={[a.gap_lg]}>
228 <View style={[a.gap_xs]}>
229 <Text style={[a.font_bold, a.text_2xl]}>
230 <Trans>Keep me posted</Trans>
231 </Text>
232 <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
233 <Trans>Get notified of this account’s activity</Trans>
234 </Text>
235 </View>
236
237 {includeProfile && (
238 <ProfileCard.Header>
239 <ProfileCard.Avatar
240 profile={profile}
241 moderationOpts={moderationOpts}
242 disabledPreview
243 />
244 <ProfileCard.NameAndHandle
245 profile={profile}
246 moderationOpts={moderationOpts}
247 />
248 </ProfileCard.Header>
249 )}
250
251 <Toggle.Group
252 label={_(msg`Subscribe to account activity`)}
253 values={values}
254 onChange={onChange}>
255 <View style={[a.gap_sm]}>
256 <Toggle.Item
257 label={_(msg`Posts`)}
258 name="post"
259 style={[
260 a.flex_1,
261 a.py_xs,
262 platform({
263 native: [a.justify_between],
264 web: [a.flex_row_reverse, a.gap_sm],
265 }),
266 ]}>
267 <Toggle.LabelText
268 style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
269 <Trans>Posts</Trans>
270 </Toggle.LabelText>
271 <Toggle.Switch />
272 </Toggle.Item>
273 <Toggle.Item
274 label={_(msg`Replies`)}
275 name="reply"
276 style={[
277 a.flex_1,
278 a.py_xs,
279 platform({
280 native: [a.justify_between],
281 web: [a.flex_row_reverse, a.gap_sm],
282 }),
283 ]}>
284 <Toggle.LabelText
285 style={[t.atoms.text, a.font_normal, a.text_md, a.flex_1]}>
286 <Trans>Replies</Trans>
287 </Toggle.LabelText>
288 <Toggle.Switch />
289 </Toggle.Item>
290 </View>
291 </Toggle.Group>
292
293 {error && (
294 <Admonition type="error">
295 <Trans>Could not save changes: {cleanError(error)}</Trans>
296 </Admonition>
297 )}
298
299 <Button {...buttonProps} size="large" variant="solid">
300 <ButtonText>{buttonProps.label}</ButtonText>
301 {isSaving && <ButtonIcon icon={Loader} />}
302 </Button>
303 </View>
304
305 <Dialog.Close />
306 </Dialog.ScrollableInner>
307 )
308}
309
310function parseActivitySubscription(
311 sub?: AppBskyNotificationDefs.ActivitySubscription,
312): Un$Typed<AppBskyNotificationDefs.ActivitySubscription> {
313 if (!sub) return {post: false, reply: false}
314 const {post, reply} = sub
315 return {post, reply}
316}