mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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 posts 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 <View style={[a.flex_1, a.gap_sm]}>
138 <Admonition.Text>
139 <Trans>
140 Enable notifications for an account by visiting their
141 profile and pressing the{' '}
142 <RNText
143 style={[a.font_bold, t.atoms.text_contrast_high]}>
144 bell icon
145 </RNText>{' '}
146 <BellRingingFilledIcon
147 size="xs"
148 style={t.atoms.text_contrast_high}
149 />
150 .
151 </Trans>
152 </Admonition.Text>
153 <Admonition.Text>
154 <Trans>
155 If you want to restrict who can receive notifications
156 for your account's activity, you can change this in{' '}
157 <InlineLinkText
158 label={_(msg`Privacy and Security settings`)}
159 to={{screen: 'ActivityPrivacySettings'}}
160 style={[a.font_bold]}>
161 Settings → Privacy and Security
162 </InlineLinkText>
163 .
164 </Trans>
165 </Admonition.Text>
166 </View>
167 </Admonition.Row>
168 </Admonition.Outer>
169 ) : (
170 <View style={[a.flex_1, a.align_center, a.pt_xl]}>
171 <Loader size="lg" />
172 </View>
173 )}
174 </View>
175 )
176 }
177 ListFooterComponent={
178 <ListFooter
179 style={[items.length === 0 && a.border_transparent]}
180 isFetchingNextPage={isFetchingNextPage}
181 error={cleanError(error)}
182 onRetry={fetchNextPage}
183 hasNextPage={hasNextPage}
184 />
185 }
186 windowSize={11}
187 />
188 </Layout.Screen>
189 )
190}
191
192function keyExtractor(item: bsky.profile.AnyProfileView) {
193 return item.did
194}
195
196function ActivitySubscriptionCard({
197 profile: profileUnshadowed,
198 moderationOpts,
199}: {
200 profile: bsky.profile.AnyProfileView
201 moderationOpts: ModerationOpts
202}) {
203 const profile = useProfileShadow(profileUnshadowed)
204 const control = useDialogControl()
205 const {_} = useLingui()
206 const t = useTheme()
207
208 const preview = useMemo(() => {
209 const actSub = profile.viewer?.activitySubscription
210 if (actSub?.post && actSub?.reply) {
211 return _(msg`Posts, Replies`)
212 } else if (actSub?.post) {
213 return _(msg`Posts`)
214 } else if (actSub?.reply) {
215 return _(msg`Replies`)
216 }
217 return _(msg`None`)
218 }, [_, profile.viewer?.activitySubscription])
219
220 return (
221 <View style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
222 <ProfileCard.Outer>
223 <ProfileCard.Header>
224 <ProfileCard.Avatar
225 profile={profile}
226 moderationOpts={moderationOpts}
227 />
228 <View style={[a.flex_1, a.gap_2xs]}>
229 <ProfileCard.NameAndHandle
230 profile={profile}
231 moderationOpts={moderationOpts}
232 inline
233 />
234 <Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
235 {preview}
236 </Text>
237 </View>
238 <Button
239 label={_(
240 msg`Edit notifications from ${createSanitizedDisplayName(
241 profile,
242 )}`,
243 )}
244 size="small"
245 color="primary"
246 variant="solid"
247 onPress={control.open}>
248 <ButtonText>
249 <Trans>Edit</Trans>
250 </ButtonText>
251 </Button>
252 </ProfileCard.Header>
253 </ProfileCard.Outer>
254
255 <SubscribeProfileDialog
256 control={control}
257 profile={profile}
258 moderationOpts={moderationOpts}
259 includeProfile
260 />
261 </View>
262 )
263}