mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at tooltip 561 lines 20 kB view raw
1import {useState} from 'react' 2import {LayoutAnimation, Pressable, View} from 'react-native' 3import {Linking} from 'react-native' 4import {useReducedMotion} from 'react-native-reanimated' 5import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' 6import {msg, t, Trans} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {useNavigation} from '@react-navigation/native' 9import {type NativeStackScreenProps} from '@react-navigation/native-stack' 10 11import {useActorStatus} from '#/lib/actor-status' 12import {IS_INTERNAL} from '#/lib/app-info' 13import {HELP_DESK_URL} from '#/lib/constants' 14import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 15import { 16 type CommonNavigatorParams, 17 type NavigationProp, 18} from '#/lib/routes/types' 19import {useGate} from '#/lib/statsig/statsig' 20import {sanitizeDisplayName} from '#/lib/strings/display-names' 21import {sanitizeHandle} from '#/lib/strings/handles' 22import {useProfileShadow} from '#/state/cache/profile-shadow' 23import * as persisted from '#/state/persisted' 24import {clearStorage} from '#/state/persisted' 25import {useModerationOpts} from '#/state/preferences/moderation-opts' 26import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' 27import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' 28import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 29import {useOnboardingDispatch} from '#/state/shell' 30import {useLoggedOutViewControls} from '#/state/shell/logged-out' 31import {useCloseAllActiveElements} from '#/state/util' 32import * as Toast from '#/view/com/util/Toast' 33import {UserAvatar} from '#/view/com/util/UserAvatar' 34import * as SettingsList from '#/screens/Settings/components/SettingsList' 35import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf' 36import {AvatarStackWithFetch} from '#/components/AvatarStack' 37import {useDialogControl} from '#/components/Dialog' 38import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 39import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 40import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' 41import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' 42import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' 43import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' 44import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 45import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 46import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 47import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' 48import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 49import { 50 Person_Stroke2_Corner2_Rounded as PersonIcon, 51 PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, 52 PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, 53 PersonX_Stroke2_Corner0_Rounded as PersonXIcon, 54} from '#/components/icons/Person' 55import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' 56import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' 57import * as Layout from '#/components/Layout' 58import {Loader} from '#/components/Loader' 59import * as Menu from '#/components/Menu' 60import * as Prompt from '#/components/Prompt' 61import {Text} from '#/components/Typography' 62import {useFullVerificationState} from '#/components/verification' 63import { 64 shouldShowVerificationCheckButton, 65 VerificationCheckButton, 66} from '#/components/verification/VerificationCheckButton' 67 68type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 69export function SettingsScreen({}: Props) { 70 const {_} = useLingui() 71 const reducedMotion = useReducedMotion() 72 const {logoutEveryAccount} = useSessionApi() 73 const {accounts, currentAccount} = useSession() 74 const switchAccountControl = useDialogControl() 75 const signOutPromptControl = Prompt.usePromptControl() 76 const {data: profile} = useProfileQuery({did: currentAccount?.did}) 77 const {data: otherProfiles} = useProfilesQuery({ 78 handles: accounts 79 .filter(acc => acc.did !== currentAccount?.did) 80 .map(acc => acc.handle), 81 }) 82 const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() 83 const [showAccounts, setShowAccounts] = useState(false) 84 const [showDevOptions, setShowDevOptions] = useState(false) 85 const gate = useGate() 86 87 return ( 88 <Layout.Screen> 89 <Layout.Header.Outer> 90 <Layout.Header.BackButton /> 91 <Layout.Header.Content> 92 <Layout.Header.TitleText> 93 <Trans>Settings</Trans> 94 </Layout.Header.TitleText> 95 </Layout.Header.Content> 96 <Layout.Header.Slot /> 97 </Layout.Header.Outer> 98 <Layout.Content> 99 <SettingsList.Container> 100 <View 101 style={[ 102 a.px_xl, 103 a.pt_md, 104 a.pb_md, 105 a.w_full, 106 a.gap_2xs, 107 a.align_center, 108 {minHeight: 160}, 109 ]}> 110 {profile && <ProfilePreview profile={profile} />} 111 </View> 112 {accounts.length > 1 ? ( 113 <> 114 <SettingsList.PressableItem 115 label={_(msg`Switch account`)} 116 accessibilityHint={_( 117 msg`Shows other accounts you can switch to`, 118 )} 119 onPress={() => { 120 if (!reducedMotion) { 121 LayoutAnimation.configureNext( 122 LayoutAnimation.Presets.easeInEaseOut, 123 ) 124 } 125 setShowAccounts(s => !s) 126 }}> 127 <SettingsList.ItemIcon icon={PersonGroupIcon} /> 128 <SettingsList.ItemText> 129 <Trans>Switch account</Trans> 130 </SettingsList.ItemText> 131 {showAccounts ? ( 132 <SettingsList.ItemIcon icon={ChevronUpIcon} size="md" /> 133 ) : ( 134 <AvatarStackWithFetch 135 profiles={accounts 136 .map(acc => acc.did) 137 .filter(did => did !== currentAccount?.did) 138 .slice(0, 5)} 139 /> 140 )} 141 </SettingsList.PressableItem> 142 {showAccounts && ( 143 <> 144 <SettingsList.Divider /> 145 {accounts 146 .filter(acc => acc.did !== currentAccount?.did) 147 .map(account => ( 148 <AccountRow 149 key={account.did} 150 account={account} 151 profile={otherProfiles?.profiles?.find( 152 p => p.did === account.did, 153 )} 154 pendingDid={pendingDid} 155 onPressSwitchAccount={onPressSwitchAccount} 156 /> 157 ))} 158 <AddAccountRow /> 159 </> 160 )} 161 </> 162 ) : ( 163 <AddAccountRow /> 164 )} 165 <SettingsList.Divider /> 166 <SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}> 167 <SettingsList.ItemIcon icon={PersonIcon} /> 168 <SettingsList.ItemText> 169 <Trans>Account</Trans> 170 </SettingsList.ItemText> 171 </SettingsList.LinkItem> 172 <SettingsList.LinkItem 173 to="/settings/privacy-and-security" 174 label={_(msg`Privacy and security`)}> 175 <SettingsList.ItemIcon icon={LockIcon} /> 176 <SettingsList.ItemText> 177 <Trans>Privacy and security</Trans> 178 </SettingsList.ItemText> 179 </SettingsList.LinkItem> 180 <SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}> 181 <SettingsList.ItemIcon icon={HandIcon} /> 182 <SettingsList.ItemText> 183 <Trans>Moderation</Trans> 184 </SettingsList.ItemText> 185 </SettingsList.LinkItem> 186 {gate('reengagement_features') && ( 187 <SettingsList.LinkItem 188 to="/settings/notifications" 189 label={_(msg`Notifications`)}> 190 <SettingsList.ItemIcon icon={NotificationIcon} /> 191 <SettingsList.ItemText> 192 <Trans>Notifications</Trans> 193 </SettingsList.ItemText> 194 </SettingsList.LinkItem> 195 )} 196 <SettingsList.LinkItem 197 to="/settings/content-and-media" 198 label={_(msg`Content and media`)}> 199 <SettingsList.ItemIcon icon={WindowIcon} /> 200 <SettingsList.ItemText> 201 <Trans>Content and media</Trans> 202 </SettingsList.ItemText> 203 </SettingsList.LinkItem> 204 <SettingsList.LinkItem 205 to="/settings/appearance" 206 label={_(msg`Appearance`)}> 207 <SettingsList.ItemIcon icon={PaintRollerIcon} /> 208 <SettingsList.ItemText> 209 <Trans>Appearance</Trans> 210 </SettingsList.ItemText> 211 </SettingsList.LinkItem> 212 <SettingsList.LinkItem 213 to="/settings/accessibility" 214 label={_(msg`Accessibility`)}> 215 <SettingsList.ItemIcon icon={AccessibilityIcon} /> 216 <SettingsList.ItemText> 217 <Trans>Accessibility</Trans> 218 </SettingsList.ItemText> 219 </SettingsList.LinkItem> 220 <SettingsList.LinkItem 221 to="/settings/language" 222 label={_(msg`Languages`)}> 223 <SettingsList.ItemIcon icon={EarthIcon} /> 224 <SettingsList.ItemText> 225 <Trans>Languages</Trans> 226 </SettingsList.ItemText> 227 </SettingsList.LinkItem> 228 <SettingsList.PressableItem 229 onPress={() => Linking.openURL(HELP_DESK_URL)} 230 label={_(msg`Help`)} 231 accessibilityHint={_(msg`Opens helpdesk in browser`)}> 232 <SettingsList.ItemIcon icon={CircleQuestionIcon} /> 233 <SettingsList.ItemText> 234 <Trans>Help</Trans> 235 </SettingsList.ItemText> 236 <SettingsList.Chevron /> 237 </SettingsList.PressableItem> 238 <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}> 239 <SettingsList.ItemIcon icon={BubbleInfoIcon} /> 240 <SettingsList.ItemText> 241 <Trans>About</Trans> 242 </SettingsList.ItemText> 243 </SettingsList.LinkItem> 244 <SettingsList.Divider /> 245 <SettingsList.PressableItem 246 destructive 247 onPress={() => signOutPromptControl.open()} 248 label={_(msg`Sign out`)}> 249 <SettingsList.ItemText> 250 <Trans>Sign out</Trans> 251 </SettingsList.ItemText> 252 </SettingsList.PressableItem> 253 {IS_INTERNAL && ( 254 <> 255 <SettingsList.Divider /> 256 <SettingsList.PressableItem 257 onPress={() => { 258 if (!reducedMotion) { 259 LayoutAnimation.configureNext( 260 LayoutAnimation.Presets.easeInEaseOut, 261 ) 262 } 263 setShowDevOptions(d => !d) 264 }} 265 label={_(msg`Developer options`)}> 266 <SettingsList.ItemIcon icon={CodeBracketsIcon} /> 267 <SettingsList.ItemText> 268 <Trans>Developer options</Trans> 269 </SettingsList.ItemText> 270 </SettingsList.PressableItem> 271 {showDevOptions && <DevOptions />} 272 </> 273 )} 274 </SettingsList.Container> 275 </Layout.Content> 276 277 <Prompt.Basic 278 control={signOutPromptControl} 279 title={_(msg`Sign out?`)} 280 description={_(msg`You will be signed out of all your accounts.`)} 281 onConfirm={() => logoutEveryAccount('Settings')} 282 confirmButtonCta={_(msg`Sign out`)} 283 cancelButtonCta={_(msg`Cancel`)} 284 confirmButtonColor="negative" 285 /> 286 287 <SwitchAccountDialog control={switchAccountControl} /> 288 </Layout.Screen> 289 ) 290} 291 292function ProfilePreview({ 293 profile, 294}: { 295 profile: AppBskyActorDefs.ProfileViewDetailed 296}) { 297 const t = useTheme() 298 const {gtMobile} = useBreakpoints() 299 const shadow = useProfileShadow(profile) 300 const moderationOpts = useModerationOpts() 301 const verificationState = useFullVerificationState({ 302 profile: shadow, 303 }) 304 const {isActive: live} = useActorStatus(profile) 305 306 if (!moderationOpts) return null 307 308 const moderation = moderateProfile(profile, moderationOpts) 309 const displayName = sanitizeDisplayName( 310 profile.displayName || sanitizeHandle(profile.handle), 311 moderation.ui('displayName'), 312 ) 313 314 return ( 315 <> 316 <UserAvatar 317 size={80} 318 avatar={shadow.avatar} 319 moderation={moderation.ui('avatar')} 320 type={shadow.associated?.labeler ? 'labeler' : 'user'} 321 live={live} 322 /> 323 324 <View 325 style={[ 326 a.flex_row, 327 a.gap_xs, 328 a.align_center, 329 a.justify_center, 330 a.w_full, 331 ]}> 332 <Text 333 emoji 334 testID="profileHeaderDisplayName" 335 numberOfLines={1} 336 style={[ 337 a.pt_sm, 338 t.atoms.text, 339 gtMobile ? a.text_4xl : a.text_3xl, 340 a.font_heavy, 341 ]}> 342 {displayName} 343 </Text> 344 {shouldShowVerificationCheckButton(verificationState) && ( 345 <View 346 style={[ 347 { 348 marginTop: platform({web: 8, ios: 8, android: 10}), 349 }, 350 ]}> 351 <VerificationCheckButton profile={shadow} size="lg" /> 352 </View> 353 )} 354 </View> 355 <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}> 356 {sanitizeHandle(profile.handle, '@')} 357 </Text> 358 </> 359 ) 360} 361 362function DevOptions() { 363 const {_} = useLingui() 364 const onboardingDispatch = useOnboardingDispatch() 365 const navigation = useNavigation<NavigationProp>() 366 const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration() 367 368 const resetOnboarding = async () => { 369 navigation.navigate('Home') 370 onboardingDispatch({type: 'start'}) 371 Toast.show(_(msg`Onboarding reset`)) 372 } 373 374 const clearAllStorage = async () => { 375 await clearStorage() 376 Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) 377 } 378 379 const onPressUnsnoozeReminder = () => { 380 const lastEmailConfirm = new Date() 381 // wind back 3 days 382 lastEmailConfirm.setDate(lastEmailConfirm.getDate() - 3) 383 persisted.write('reminders', { 384 ...persisted.get('reminders'), 385 lastEmailConfirm: lastEmailConfirm.toISOString(), 386 }) 387 Toast.show(t`You probably want to restart the app now.`) 388 } 389 390 return ( 391 <> 392 <SettingsList.PressableItem 393 onPress={() => navigation.navigate('Log')} 394 label={_(msg`Open system log`)}> 395 <SettingsList.ItemText> 396 <Trans>System log</Trans> 397 </SettingsList.ItemText> 398 </SettingsList.PressableItem> 399 <SettingsList.PressableItem 400 onPress={() => navigation.navigate('Debug')} 401 label={_(msg`Open storybook page`)}> 402 <SettingsList.ItemText> 403 <Trans>Storybook</Trans> 404 </SettingsList.ItemText> 405 </SettingsList.PressableItem> 406 <SettingsList.PressableItem 407 onPress={() => navigation.navigate('DebugMod')} 408 label={_(msg`Open moderation debug page`)}> 409 <SettingsList.ItemText> 410 <Trans>Debug Moderation</Trans> 411 </SettingsList.ItemText> 412 </SettingsList.PressableItem> 413 <SettingsList.PressableItem 414 onPress={() => deleteChatDeclarationRecord()} 415 label={_(msg`Open storybook page`)}> 416 <SettingsList.ItemText> 417 <Trans>Delete chat declaration record</Trans> 418 </SettingsList.ItemText> 419 </SettingsList.PressableItem> 420 <SettingsList.PressableItem 421 onPress={() => resetOnboarding()} 422 label={_(msg`Reset onboarding state`)}> 423 <SettingsList.ItemText> 424 <Trans>Reset onboarding state</Trans> 425 </SettingsList.ItemText> 426 </SettingsList.PressableItem> 427 <SettingsList.PressableItem 428 onPress={onPressUnsnoozeReminder} 429 label={_(msg`Unsnooze email reminder`)}> 430 <SettingsList.ItemText> 431 <Trans>Unsnooze email reminder</Trans> 432 </SettingsList.ItemText> 433 </SettingsList.PressableItem> 434 <SettingsList.PressableItem 435 onPress={() => clearAllStorage()} 436 label={_(msg`Clear all storage data`)}> 437 <SettingsList.ItemText> 438 <Trans>Clear all storage data (restart after this)</Trans> 439 </SettingsList.ItemText> 440 </SettingsList.PressableItem> 441 </> 442 ) 443} 444 445function AddAccountRow() { 446 const {_} = useLingui() 447 const {setShowLoggedOut} = useLoggedOutViewControls() 448 const closeEverything = useCloseAllActiveElements() 449 450 const onAddAnotherAccount = () => { 451 setShowLoggedOut(true) 452 closeEverything() 453 } 454 455 return ( 456 <SettingsList.PressableItem 457 onPress={onAddAnotherAccount} 458 label={_(msg`Add another account`)}> 459 <SettingsList.ItemIcon icon={PersonPlusIcon} /> 460 <SettingsList.ItemText> 461 <Trans>Add another account</Trans> 462 </SettingsList.ItemText> 463 </SettingsList.PressableItem> 464 ) 465} 466 467function AccountRow({ 468 profile, 469 account, 470 pendingDid, 471 onPressSwitchAccount, 472}: { 473 profile?: AppBskyActorDefs.ProfileViewDetailed 474 account: SessionAccount 475 pendingDid: string | null 476 onPressSwitchAccount: ( 477 account: SessionAccount, 478 logContext: 'Settings', 479 ) => void 480}) { 481 const {_} = useLingui() 482 const t = useTheme() 483 484 const moderationOpts = useModerationOpts() 485 const removePromptControl = Prompt.usePromptControl() 486 const {removeAccount} = useSessionApi() 487 const {isActive: live} = useActorStatus(profile) 488 489 const onSwitchAccount = () => { 490 if (pendingDid) return 491 onPressSwitchAccount(account, 'Settings') 492 } 493 494 return ( 495 <View style={[a.relative]}> 496 <SettingsList.PressableItem 497 onPress={onSwitchAccount} 498 label={_(msg`Switch account`)}> 499 {moderationOpts && profile ? ( 500 <UserAvatar 501 size={28} 502 avatar={profile.avatar} 503 moderation={moderateProfile(profile, moderationOpts).ui('avatar')} 504 type={profile.associated?.labeler ? 'labeler' : 'user'} 505 live={live} 506 hideLiveBadge 507 /> 508 ) : ( 509 <View style={[{width: 28}]} /> 510 )} 511 <SettingsList.ItemText> 512 {sanitizeHandle(account.handle, '@')} 513 </SettingsList.ItemText> 514 {pendingDid === account.did && <SettingsList.ItemIcon icon={Loader} />} 515 </SettingsList.PressableItem> 516 {!pendingDid && ( 517 <Menu.Root> 518 <Menu.Trigger label={_(msg`Account options`)}> 519 {({props, state}) => ( 520 <Pressable 521 {...props} 522 style={[ 523 a.absolute, 524 {top: 10, right: tokens.space.lg}, 525 a.p_xs, 526 a.rounded_full, 527 (state.hovered || state.pressed) && t.atoms.bg_contrast_25, 528 ]}> 529 <DotsHorizontal size="md" style={t.atoms.text} /> 530 </Pressable> 531 )} 532 </Menu.Trigger> 533 <Menu.Outer showCancel> 534 <Menu.Item 535 label={_(msg`Remove account`)} 536 onPress={() => removePromptControl.open()}> 537 <Menu.ItemText> 538 <Trans>Remove account</Trans> 539 </Menu.ItemText> 540 <Menu.ItemIcon icon={PersonXIcon} /> 541 </Menu.Item> 542 </Menu.Outer> 543 </Menu.Root> 544 )} 545 546 <Prompt.Basic 547 control={removePromptControl} 548 title={_(msg`Remove from quick access?`)} 549 description={_( 550 msg`This will remove @${account.handle} from the quick access list.`, 551 )} 552 onConfirm={() => { 553 removeAccount(account) 554 Toast.show(_(msg`Account removed from quick access`)) 555 }} 556 confirmButtonCta={_(msg`Remove`)} 557 confirmButtonColor="negative" 558 /> 559 </View> 560 ) 561}