Bluesky app fork with some witchin' additions 馃挮
at main 269 lines 9.1 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} 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 &rarr; 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}