Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 266 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, 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 &rarr; 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}