forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useMemo} from 'react'
2import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
3import {type ModerationOpts} from '@atproto/api'
4import {msg} from '@lingui/core/macro'
5import {useLingui} from '@lingui/react'
6import {Trans} from '@lingui/react/macro'
7
8import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
9import {
10 type AllNavigatorParams,
11 type NativeStackScreenProps,
12} from '#/lib/routes/types'
13import {cleanError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15import {useProfileShadow} from '#/state/cache/profile-shadow'
16import {useModerationOpts} from '#/state/preferences/moderation-opts'
17import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscriptions'
18import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
19import {List} from '#/view/com/util/List'
20import {atoms as a, useTheme} from '#/alf'
21import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog'
22import * as Admonition from '#/components/Admonition'
23import {Button, ButtonText} from '#/components/Button'
24import {useDialogControl} from '#/components/Dialog'
25import {
26 BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon,
27 BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon,
28} from '#/components/icons/BellRinging'
29import * as Layout from '#/components/Layout'
30import {InlineLinkText} from '#/components/Link'
31import {ListFooter} from '#/components/Lists'
32import {Loader} from '#/components/Loader'
33import * as ProfileCard from '#/components/ProfileCard'
34import {Text} from '#/components/Typography'
35import type * as bsky from '#/types/bsky'
36import * as SettingsList from '../components/SettingsList'
37import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
38import {PreferenceControls} from './components/PreferenceControls'
39
40type Props = NativeStackScreenProps<
41 AllNavigatorParams,
42 'ActivityNotificationSettings'
43>
44export function ActivityNotificationSettingsScreen({}: Props) {
45 const t = useTheme()
46 const {_} = useLingui()
47 const {data: preferences, isError} = useNotificationSettingsQuery()
48
49 const moderationOpts = useModerationOpts()
50
51 const {
52 data: subscriptions,
53 isPending,
54 error,
55 isFetchingNextPage,
56 fetchNextPage,
57 hasNextPage,
58 } = useActivitySubscriptionsQuery()
59
60 const items = useMemo(() => {
61 if (!subscriptions) return []
62 return subscriptions?.pages.flatMap(page => page.subscriptions)
63 }, [subscriptions])
64
65 const renderItem = useCallback(
66 ({item}: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
67 if (!moderationOpts) return null
68 return (
69 <ActivitySubscriptionCard
70 profile={item}
71 moderationOpts={moderationOpts}
72 />
73 )
74 },
75 [moderationOpts],
76 )
77
78 const onEndReached = useCallback(async () => {
79 if (isFetchingNextPage || !hasNextPage || isError) return
80 try {
81 await fetchNextPage()
82 } catch (err) {
83 logger.error('Failed to load more likes', {message: err})
84 }
85 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
86
87 return (
88 <Layout.Screen>
89 <Layout.Header.Outer>
90 <Layout.Header.BackButton />
91 <Layout.Header.Content>
92 <Layout.Header.TitleText>
93 <Trans>Notifications</Trans>
94 </Layout.Header.TitleText>
95 </Layout.Header.Content>
96 <Layout.Header.Slot />
97 </Layout.Header.Outer>
98 <List
99 ListHeaderComponent={
100 <SettingsList.Container>
101 <SettingsList.Item style={[a.align_start]}>
102 <SettingsList.ItemIcon icon={BellRingingIcon} />
103 <ItemTextWithSubtitle
104 bold
105 titleText={<Trans>Activity from others</Trans>}
106 subtitleText={
107 <Trans>
108 Get notified about posts and replies from accounts you
109 choose.
110 </Trans>
111 }
112 />
113 </SettingsList.Item>
114 {isError ? (
115 <View style={[a.px_lg, a.pt_md]}>
116 <Admonition.Admonition type="error">
117 <Trans>Failed to load notification settings.</Trans>
118 </Admonition.Admonition>
119 </View>
120 ) : (
121 <PreferenceControls
122 name="subscribedPost"
123 preference={preferences?.subscribedPost}
124 />
125 )}
126 </SettingsList.Container>
127 }
128 data={items}
129 keyExtractor={keyExtractor}
130 renderItem={renderItem}
131 onEndReached={onEndReached}
132 onEndReachedThreshold={4}
133 ListEmptyComponent={
134 error ? null : (
135 <View style={[a.px_xl, a.py_md]}>
136 {!isPending ? (
137 <Admonition.Outer type="tip">
138 <Admonition.Row>
139 <Admonition.Icon />
140 <Admonition.Content>
141 <Admonition.Text>
142 <Trans>
143 Enable notifications for an account by visiting their
144 profile and pressing the{' '}
145 <RNText
146 style={[
147 a.font_semi_bold,
148 t.atoms.text_contrast_high,
149 ]}>
150 bell icon
151 </RNText>{' '}
152 <BellRingingFilledIcon
153 size="xs"
154 style={t.atoms.text_contrast_high}
155 />
156 .
157 </Trans>
158 </Admonition.Text>
159 <Admonition.Text>
160 <Trans>
161 If you want to restrict who can receive notifications
162 for your account's activity, you can change this in{' '}
163 <InlineLinkText
164 label={_(msg`Privacy and Security settings`)}
165 to={{screen: 'ActivityPrivacySettings'}}
166 style={[a.font_semi_bold]}>
167 Settings → Privacy and Security
168 </InlineLinkText>
169 .
170 </Trans>
171 </Admonition.Text>
172 </Admonition.Content>
173 </Admonition.Row>
174 </Admonition.Outer>
175 ) : (
176 <View style={[a.flex_1, a.align_center, a.pt_xl]}>
177 <Loader size="lg" />
178 </View>
179 )}
180 </View>
181 )
182 }
183 ListFooterComponent={
184 <ListFooter
185 style={[items.length === 0 && a.border_transparent]}
186 isFetchingNextPage={isFetchingNextPage}
187 error={cleanError(error)}
188 onRetry={fetchNextPage}
189 hasNextPage={hasNextPage}
190 />
191 }
192 windowSize={11}
193 />
194 </Layout.Screen>
195 )
196}
197
198function keyExtractor(item: bsky.profile.AnyProfileView) {
199 return item.did
200}
201
202function ActivitySubscriptionCard({
203 profile: profileUnshadowed,
204 moderationOpts,
205}: {
206 profile: bsky.profile.AnyProfileView
207 moderationOpts: ModerationOpts
208}) {
209 const profile = useProfileShadow(profileUnshadowed)
210 const control = useDialogControl()
211 const {_} = useLingui()
212 const t = useTheme()
213
214 const preview = useMemo(() => {
215 const actSub = profile.viewer?.activitySubscription
216 if (actSub?.post && actSub?.reply) {
217 return _(msg`Posts, Replies`)
218 } else if (actSub?.post) {
219 return _(msg`Posts`)
220 } else if (actSub?.reply) {
221 return _(msg`Replies`)
222 }
223 return _(msg`None`)
224 }, [_, profile.viewer?.activitySubscription])
225
226 return (
227 <View style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
228 <ProfileCard.Outer>
229 <ProfileCard.Header>
230 <ProfileCard.Avatar
231 profile={profile}
232 moderationOpts={moderationOpts}
233 />
234 <View style={[a.flex_1, a.gap_2xs]}>
235 <ProfileCard.NameAndHandle
236 profile={profile}
237 moderationOpts={moderationOpts}
238 inline
239 />
240 <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
241 {preview}
242 </Text>
243 </View>
244 <Button
245 label={_(
246 msg`Edit notifications from ${createSanitizedDisplayName(
247 profile,
248 )}`,
249 )}
250 size="small"
251 color="primary"
252 variant="solid"
253 onPress={control.open}>
254 <ButtonText>
255 <Trans>Edit</Trans>
256 </ButtonText>
257 </Button>
258 </ProfileCard.Header>
259 </ProfileCard.Outer>
260
261 <SubscribeProfileDialog
262 control={control}
263 profile={profile}
264 moderationOpts={moderationOpts}
265 includeProfile
266 />
267 </View>
268 )
269}