mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at sys-log 263 lines 9.0 kB view raw
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 &rarr; 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}