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