my fork of the bluesky client

[Settings] Ungate, and remove old settings (#6144)

* move export car dialog

* move disableemail2fadialog

* delete old settings screens

* fix type error

* Update Navigation.tsx

* Delete AccountDropdownBtn.tsx

* remove old change handle modal

* delete add app paswords

* forgot to actually delete the change handle modal

authored by samuel.fm and committed by

GitHub 5da3f294 e8a03058

+16 -3768
+11 -11
src/Navigation.tsx
··· 40 40 shouldRequestEmailConfirmation, 41 41 snoozeEmailConfirmationPrompt, 42 42 } from '#/state/shell/reminders' 43 - import {AccessibilitySettingsScreen} from '#/view/screens/AccessibilitySettings' 44 - import {AppPasswords} from '#/view/screens/AppPasswords' 45 43 import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines' 46 44 import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy' 47 45 import {DebugModScreen} from '#/view/screens/DebugMod' 48 46 import {FeedsScreen} from '#/view/screens/Feeds' 49 47 import {HomeScreen} from '#/view/screens/Home' 50 - import {LanguageSettingsScreen} from '#/view/screens/LanguageSettings' 51 48 import {ListsScreen} from '#/view/screens/Lists' 52 49 import {LogScreen} from '#/view/screens/Log' 53 50 import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts' ··· 56 53 import {NotFoundScreen} from '#/view/screens/NotFound' 57 54 import {NotificationsScreen} from '#/view/screens/Notifications' 58 55 import {PostThreadScreen} from '#/view/screens/PostThread' 59 - import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds' 60 - import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed' 61 - import {PreferencesThreads} from '#/view/screens/PreferencesThreads' 62 56 import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' 63 57 import {ProfileScreen} from '#/view/screens/Profile' 64 58 import {ProfileFeedScreen} from '#/view/screens/ProfileFeed' ··· 68 62 import {ProfileListScreen} from '#/view/screens/ProfileList' 69 63 import {SavedFeeds} from '#/view/screens/SavedFeeds' 70 64 import {SearchScreen} from '#/view/screens/Search' 71 - import {SettingsScreen} from '#/view/screens/Settings' 72 65 import {Storybook} from '#/view/screens/Storybook' 73 66 import {SupportScreen} from '#/view/screens/Support' 74 67 import {TermsOfServiceScreen} from '#/view/screens/TermsOfService' ··· 96 89 import {router} from '#/routes' 97 90 import {Referrer} from '../modules/expo-bluesky-swiss-army' 98 91 import {AboutSettingsScreen} from './screens/Settings/AboutSettings' 92 + import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' 99 93 import {AccountSettingsScreen} from './screens/Settings/AccountSettings' 94 + import {AppPasswordsScreen} from './screens/Settings/AppPasswords' 100 95 import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' 96 + import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' 97 + import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' 98 + import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' 101 99 import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' 100 + import {SettingsScreen} from './screens/Settings/Settings' 101 + import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' 102 102 103 103 const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 104 104 ··· 285 285 /> 286 286 <Stack.Screen 287 287 name="AppPasswords" 288 - getComponent={() => AppPasswords} 288 + getComponent={() => AppPasswordsScreen} 289 289 options={{title: title(msg`App Passwords`), requireAuth: true}} 290 290 /> 291 291 <Stack.Screen ··· 295 295 /> 296 296 <Stack.Screen 297 297 name="PreferencesFollowingFeed" 298 - getComponent={() => PreferencesFollowingFeed} 298 + getComponent={() => FollowingFeedPreferencesScreen} 299 299 options={{ 300 300 title: title(msg`Following Feed Preferences`), 301 301 requireAuth: true, ··· 303 303 /> 304 304 <Stack.Screen 305 305 name="PreferencesThreads" 306 - getComponent={() => PreferencesThreads} 306 + getComponent={() => ThreadPreferencesScreen} 307 307 options={{title: title(msg`Threads Preferences`), requireAuth: true}} 308 308 /> 309 309 <Stack.Screen 310 310 name="PreferencesExternalEmbeds" 311 - getComponent={() => PreferencesExternalEmbeds} 311 + getComponent={() => ExternalMediaPreferencesScreen} 312 312 options={{ 313 313 title: title(msg`External Media Preferences`), 314 314 requireAuth: true,
-14
src/lib/hooks/useCustomPalette.ts
··· 1 - import React from 'react' 2 - 3 - import {choose} from '#/lib/functions' 4 - import {useTheme} from '#/lib/ThemeContext' 5 - 6 - export function useCustomPalette<T>({light, dark}: {light: T; dark: T}) { 7 - const theme = useTheme() 8 - return React.useMemo(() => { 9 - return choose<T, Record<string, T>>(theme.colorScheme, { 10 - dark, 11 - light, 12 - }) 13 - }, [theme.colorScheme, dark, light]) 14 - }
-131
src/screens/Moderation/index.tsx
··· 1 1 import React from 'react' 2 2 import {Linking, View} from 'react-native' 3 3 import {useSafeAreaFrame} from 'react-native-safe-area-context' 4 - import {ComAtprotoLabelDefs} from '@atproto/api' 5 4 import {LABELS} from '@atproto/api' 6 5 import {msg, Trans} from '@lingui/macro' 7 6 import {useLingui} from '@lingui/react' 8 7 import {useFocusEffect} from '@react-navigation/native' 9 8 10 - import {IS_INTERNAL} from '#/lib/app-info' 11 9 import {getLabelingServiceTitle} from '#/lib/moderation' 12 10 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 13 11 import {logger} from '#/logger' ··· 18 16 UsePreferencesQueryResponse, 19 17 usePreferencesSetAdultContentMutation, 20 18 } from '#/state/queries/preferences' 21 - import { 22 - useProfileQuery, 23 - useProfileUpdateMutation, 24 - } from '#/state/queries/profile' 25 - import {useSession} from '#/state/session' 26 19 import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' 27 20 import {useSetMinimalShellMode} from '#/state/shell' 28 21 import {ViewHeader} from '#/view/com/util/ViewHeader' ··· 469 462 })} 470 463 </View> 471 464 )} 472 - 473 - {!IS_INTERNAL && ( 474 - <> 475 - <Text 476 - style={[ 477 - a.text_md, 478 - a.font_bold, 479 - a.pt_2xl, 480 - a.pb_md, 481 - t.atoms.text_contrast_high, 482 - ]}> 483 - <Trans>Logged-out visibility</Trans> 484 - </Text> 485 - 486 - <PwiOptOut /> 487 - </> 488 - )} 489 - 490 465 <View style={{height: 200}} /> 491 466 </ScrollView> 492 467 ) 493 468 } 494 - 495 - function PwiOptOut() { 496 - const t = useTheme() 497 - const {_} = useLingui() 498 - const {currentAccount} = useSession() 499 - const {data: profile} = useProfileQuery({did: currentAccount?.did}) 500 - const updateProfile = useProfileUpdateMutation() 501 - 502 - const isOptedOut = 503 - profile?.labels?.some(l => l.val === '!no-unauthenticated') || false 504 - const canToggle = profile && !updateProfile.isPending 505 - 506 - const onToggleOptOut = React.useCallback(() => { 507 - if (!profile) { 508 - return 509 - } 510 - let wasAdded = false 511 - updateProfile.mutate({ 512 - profile, 513 - updates: existing => { 514 - // create labels attr if needed 515 - existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) 516 - ? existing.labels 517 - : { 518 - $type: 'com.atproto.label.defs#selfLabels', 519 - values: [], 520 - } 521 - 522 - // toggle the label 523 - const hasLabel = existing.labels.values.some( 524 - l => l.val === '!no-unauthenticated', 525 - ) 526 - if (hasLabel) { 527 - wasAdded = false 528 - existing.labels.values = existing.labels.values.filter( 529 - l => l.val !== '!no-unauthenticated', 530 - ) 531 - } else { 532 - wasAdded = true 533 - existing.labels.values.push({val: '!no-unauthenticated'}) 534 - } 535 - 536 - // delete if no longer needed 537 - if (existing.labels.values.length === 0) { 538 - delete existing.labels 539 - } 540 - return existing 541 - }, 542 - checkCommitted: res => { 543 - const exists = !!res.data.labels?.some( 544 - l => l.val === '!no-unauthenticated', 545 - ) 546 - return exists === wasAdded 547 - }, 548 - }) 549 - }, [updateProfile, profile]) 550 - 551 - return ( 552 - <View style={[a.pt_sm]}> 553 - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> 554 - <Toggle.Item 555 - disabled={!canToggle} 556 - value={isOptedOut} 557 - onChange={onToggleOptOut} 558 - name="logged_out_visibility" 559 - style={a.flex_1} 560 - label={_( 561 - msg`Discourage apps from showing my account to logged-out users`, 562 - )}> 563 - <Toggle.Switch /> 564 - <Toggle.LabelText style={[a.text_md, a.flex_1]}> 565 - <Trans> 566 - Discourage apps from showing my account to logged-out users 567 - </Trans> 568 - </Toggle.LabelText> 569 - </Toggle.Item> 570 - 571 - {updateProfile.isPending && <Loader />} 572 - </View> 573 - 574 - <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}> 575 - <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> 576 - <Trans> 577 - Bluesky will not show your profile and posts to logged-out users. 578 - Other apps may not honor this request. This does not make your 579 - account private. 580 - </Trans> 581 - </Text> 582 - <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> 583 - <Trans> 584 - Note: Bluesky is an open and public network. This setting only 585 - limits the visibility of your content on the Bluesky app and 586 - website, and other apps may not respect this setting. Your content 587 - may still be shown to logged-out users by other apps and websites. 588 - </Trans> 589 - </Text> 590 - 591 - <InlineLinkText 592 - label={_(msg`Learn more about what is public on Bluesky.`)} 593 - to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> 594 - <Trans>Learn more about what is public on Bluesky.</Trans> 595 - </InlineLinkText> 596 - </View> 597 - </View> 598 - ) 599 - }
+1 -1
src/screens/Settings/AccountSettings.tsx
··· 6 6 import {CommonNavigatorParams} from '#/lib/routes/types' 7 7 import {useModalControls} from '#/state/modals' 8 8 import {useSession} from '#/state/session' 9 - import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog' 10 9 import * as SettingsList from '#/screens/Settings/components/SettingsList' 11 10 import {atoms as a, useTheme} from '#/alf' 12 11 import {useDialogControl} from '#/components/Dialog' ··· 24 23 import * as Layout from '#/components/Layout' 25 24 import {ChangeHandleDialog} from './components/ChangeHandleDialog' 26 25 import {DeactivateAccountDialog} from './components/DeactivateAccountDialog' 26 + import {ExportCarDialog} from './components/ExportCarDialog' 27 27 28 28 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'> 29 29 export function AccountSettingsScreen({}: Props) {
+1 -1
src/screens/Settings/components/Email2FAToggle.tsx
··· 4 4 5 5 import {useModalControls} from '#/state/modals' 6 6 import {useAgent, useSession} from '#/state/session' 7 - import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog' 8 7 import {useDialogControl} from '#/components/Dialog' 9 8 import * as Prompt from '#/components/Prompt' 9 + import {DisableEmail2FADialog} from './DisableEmail2FADialog' 10 10 import * as SettingsList from './SettingsList' 11 11 12 12 export function Email2FAToggle() {
-11
src/state/modals/index.tsx
··· 48 48 name: 'delete-account' 49 49 } 50 50 51 - export interface ChangeHandleModal { 52 - name: 'change-handle' 53 - onChanged: () => void 54 - } 55 - 56 51 export interface WaitlistModal { 57 52 name: 'waitlist' 58 53 } 59 54 60 55 export interface InviteCodesModal { 61 56 name: 'invite-codes' 62 - } 63 - 64 - export interface AddAppPasswordModal { 65 - name: 'add-app-password' 66 57 } 67 58 68 59 export interface ContentLanguagesSettingsModal { ··· 101 92 102 93 export type Modal = 103 94 // Account 104 - | AddAppPasswordModal 105 - | ChangeHandleModal 106 95 | DeleteAccountModal 107 96 | VerifyEmailModal 108 97 | ChangeEmailModal
-307
src/view/com/modals/AddAppPasswords.tsx
··· 1 - import React, {useState} from 'react' 2 - import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native' 3 - import {setStringAsync} from 'expo-clipboard' 4 - import { 5 - FontAwesomeIcon, 6 - FontAwesomeIconStyle, 7 - } from '@fortawesome/react-native-fontawesome' 8 - import {msg, Trans} from '@lingui/macro' 9 - import {useLingui} from '@lingui/react' 10 - 11 - import {usePalette} from '#/lib/hooks/usePalette' 12 - import {s} from '#/lib/styles' 13 - import {logger} from '#/logger' 14 - import {isNative} from '#/platform/detection' 15 - import {useModalControls} from '#/state/modals' 16 - import { 17 - useAppPasswordCreateMutation, 18 - useAppPasswordsQuery, 19 - } from '#/state/queries/app-passwords' 20 - import {Button} from '#/view/com/util/forms/Button' 21 - import {Text} from '#/view/com/util/text/Text' 22 - import * as Toast from '#/view/com/util/Toast' 23 - import {atoms as a} from '#/alf' 24 - import * as Toggle from '#/components/forms/Toggle' 25 - 26 - export const snapPoints = ['90%'] 27 - 28 - const shadesOfBlue: string[] = [ 29 - 'AliceBlue', 30 - 'Aqua', 31 - 'Aquamarine', 32 - 'Azure', 33 - 'BabyBlue', 34 - 'Blue', 35 - 'BlueViolet', 36 - 'CadetBlue', 37 - 'CornflowerBlue', 38 - 'Cyan', 39 - 'DarkBlue', 40 - 'DarkCyan', 41 - 'DarkSlateBlue', 42 - 'DeepSkyBlue', 43 - 'DodgerBlue', 44 - 'ElectricBlue', 45 - 'LightBlue', 46 - 'LightCyan', 47 - 'LightSkyBlue', 48 - 'LightSteelBlue', 49 - 'MediumAquaMarine', 50 - 'MediumBlue', 51 - 'MediumSlateBlue', 52 - 'MidnightBlue', 53 - 'Navy', 54 - 'PowderBlue', 55 - 'RoyalBlue', 56 - 'SkyBlue', 57 - 'SlateBlue', 58 - 'SteelBlue', 59 - 'Teal', 60 - 'Turquoise', 61 - ] 62 - 63 - export function Component({}: {}) { 64 - const pal = usePalette('default') 65 - const {_} = useLingui() 66 - const {closeModal} = useModalControls() 67 - const {data: passwords} = useAppPasswordsQuery() 68 - const {mutateAsync: mutateAppPassword, isPending} = 69 - useAppPasswordCreateMutation() 70 - const [name, setName] = useState( 71 - shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], 72 - ) 73 - const [appPassword, setAppPassword] = useState<string>() 74 - const [wasCopied, setWasCopied] = useState(false) 75 - const [privileged, setPrivileged] = useState(false) 76 - 77 - const onCopy = React.useCallback(() => { 78 - if (appPassword) { 79 - setStringAsync(appPassword) 80 - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 81 - setWasCopied(true) 82 - } 83 - }, [appPassword, _]) 84 - 85 - const onDone = React.useCallback(() => { 86 - closeModal() 87 - }, [closeModal]) 88 - 89 - const createAppPassword = async () => { 90 - // if name is all whitespace, we don't allow it 91 - if (!name || !name.trim()) { 92 - Toast.show( 93 - _( 94 - msg`Please enter a name for your app password. All spaces is not allowed.`, 95 - ), 96 - 'xmark', 97 - ) 98 - return 99 - } 100 - // if name is too short (under 4 chars), we don't allow it 101 - if (name.length < 4) { 102 - Toast.show( 103 - _(msg`App Password names must be at least 4 characters long.`), 104 - 'xmark', 105 - ) 106 - return 107 - } 108 - 109 - if (passwords?.find(p => p.name === name)) { 110 - Toast.show(_(msg`This name is already in use`), 'xmark') 111 - return 112 - } 113 - 114 - try { 115 - const newPassword = await mutateAppPassword({name, privileged}) 116 - if (newPassword) { 117 - setAppPassword(newPassword.password) 118 - } else { 119 - Toast.show(_(msg`Failed to create app password.`), 'xmark') 120 - // TODO: better error handling (?) 121 - } 122 - } catch (e) { 123 - Toast.show(_(msg`Failed to create app password.`), 'xmark') 124 - logger.error('Failed to create app password', {message: e}) 125 - } 126 - } 127 - 128 - const _onChangeText = (text: string) => { 129 - // sanitize input 130 - // we only all alphanumeric characters, spaces, dashes, and underscores 131 - // if the user enters anything else, we ignore it and shake the input container 132 - // also, it cannot start with a space 133 - if (text.match(/^[a-zA-Z0-9-_ ]*$/)) { 134 - setName(text) 135 - } else { 136 - Toast.show( 137 - _( 138 - msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`, 139 - ), 140 - 'xmark', 141 - ) 142 - } 143 - } 144 - 145 - return ( 146 - <View style={[styles.container, pal.view]} testID="addAppPasswordsModal"> 147 - {!appPassword ? ( 148 - <> 149 - <View> 150 - <Text type="lg" style={[pal.text]}> 151 - <Trans> 152 - Please enter a unique name for this App Password or use our 153 - randomly generated one. 154 - </Trans> 155 - </Text> 156 - <View style={[pal.btn, styles.textInputWrapper]}> 157 - <TextInput 158 - style={[styles.input, pal.text]} 159 - onChangeText={_onChangeText} 160 - value={name} 161 - placeholder={_(msg`Enter a name for this App Password`)} 162 - placeholderTextColor={pal.colors.textLight} 163 - autoCorrect={false} 164 - autoComplete="off" 165 - autoCapitalize="none" 166 - autoFocus={true} 167 - maxLength={32} 168 - selectTextOnFocus={true} 169 - blurOnSubmit={true} 170 - editable={!isPending} 171 - returnKeyType="done" 172 - onSubmitEditing={createAppPassword} 173 - accessible={true} 174 - accessibilityLabel={_(msg`Name`)} 175 - accessibilityHint={_(msg`Input name for app password`)} 176 - /> 177 - </View> 178 - </View> 179 - <Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}> 180 - <Trans> 181 - Can only contain letters, numbers, spaces, dashes, and 182 - underscores. Must be at least 4 characters long, but no more than 183 - 32 characters long. 184 - </Trans> 185 - </Text> 186 - <Toggle.Item 187 - type="checkbox" 188 - label={_(msg`Allow access to your direct messages`)} 189 - value={privileged} 190 - onChange={val => setPrivileged(val)} 191 - name="privileged" 192 - style={a.my_md}> 193 - <Toggle.Checkbox /> 194 - <Toggle.LabelText> 195 - <Trans>Allow access to your direct messages</Trans> 196 - </Toggle.LabelText> 197 - </Toggle.Item> 198 - </> 199 - ) : ( 200 - <> 201 - <View> 202 - <Text type="lg" style={[pal.text]}> 203 - <Text type="lg-bold" style={[pal.text, s.mr5]}> 204 - <Trans>Here is your app password.</Trans> 205 - </Text> 206 - <Trans> 207 - Use this to sign into the other app along with your handle. 208 - </Trans> 209 - </Text> 210 - <TouchableOpacity 211 - style={[pal.border, styles.passwordContainer, pal.btn]} 212 - onPress={onCopy} 213 - accessibilityRole="button" 214 - accessibilityLabel={_(msg`Copy`)} 215 - accessibilityHint={_(msg`Copies app password`)}> 216 - <Text type="2xl-bold" style={[pal.text]}> 217 - {appPassword} 218 - </Text> 219 - {wasCopied ? ( 220 - <Text style={[pal.textLight]}> 221 - <Trans>Copied</Trans> 222 - </Text> 223 - ) : ( 224 - <FontAwesomeIcon 225 - icon={['far', 'clone']} 226 - style={pal.text as FontAwesomeIconStyle} 227 - size={18} 228 - /> 229 - )} 230 - </TouchableOpacity> 231 - </View> 232 - <Text type="lg" style={[pal.textLight, s.mb10]}> 233 - <Trans> 234 - For security reasons, you won't be able to view this again. If you 235 - lose this password, you'll need to generate a new one. 236 - </Trans> 237 - </Text> 238 - </> 239 - )} 240 - <View style={styles.btnContainer}> 241 - <Button 242 - type="primary" 243 - label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)} 244 - style={styles.btn} 245 - labelStyle={styles.btnLabel} 246 - onPress={!appPassword ? createAppPassword : onDone} 247 - /> 248 - </View> 249 - </View> 250 - ) 251 - } 252 - 253 - const styles = StyleSheet.create({ 254 - container: { 255 - flex: 1, 256 - paddingBottom: isNative ? 50 : 0, 257 - paddingHorizontal: 16, 258 - }, 259 - textInputWrapper: { 260 - borderRadius: 8, 261 - flexDirection: 'row', 262 - alignItems: 'center', 263 - marginTop: 16, 264 - marginBottom: 8, 265 - }, 266 - input: { 267 - flex: 1, 268 - width: '100%', 269 - paddingVertical: 10, 270 - paddingHorizontal: 8, 271 - fontSize: 17, 272 - letterSpacing: 0.25, 273 - fontWeight: '400', 274 - borderRadius: 10, 275 - }, 276 - passwordContainer: { 277 - flexDirection: 'row', 278 - justifyContent: 'space-between', 279 - paddingVertical: 8, 280 - paddingHorizontal: 16, 281 - alignItems: 'center', 282 - borderRadius: 10, 283 - marginTop: 16, 284 - marginBottom: 12, 285 - }, 286 - btnContainer: { 287 - flexDirection: 'row', 288 - justifyContent: 'center', 289 - marginTop: 12, 290 - }, 291 - btn: { 292 - flexDirection: 'row', 293 - alignItems: 'center', 294 - justifyContent: 'center', 295 - borderRadius: 32, 296 - paddingHorizontal: 60, 297 - paddingVertical: 14, 298 - }, 299 - btnLabel: { 300 - fontSize: 18, 301 - }, 302 - groupContent: { 303 - borderTopWidth: 1, 304 - flexDirection: 'row', 305 - alignItems: 'center', 306 - }, 307 - })
-614
src/view/com/modals/ChangeHandle.tsx
··· 1 - import React, {useState} from 'react' 2 - import { 3 - ActivityIndicator, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 8 - import {setStringAsync} from 'expo-clipboard' 9 - import {ComAtprotoServerDescribeServer} from '@atproto/api' 10 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 - import {msg, Trans} from '@lingui/macro' 12 - import {useLingui} from '@lingui/react' 13 - 14 - import {usePalette} from '#/lib/hooks/usePalette' 15 - import {cleanError} from '#/lib/strings/errors' 16 - import {createFullHandle, makeValidHandle} from '#/lib/strings/handles' 17 - import {s} from '#/lib/styles' 18 - import {useTheme} from '#/lib/ThemeContext' 19 - import {logger} from '#/logger' 20 - import {useModalControls} from '#/state/modals' 21 - import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle' 22 - import {useServiceQuery} from '#/state/queries/service' 23 - import {SessionAccount, useAgent, useSession} from '#/state/session' 24 - import {ErrorMessage} from '../util/error/ErrorMessage' 25 - import {Button} from '../util/forms/Button' 26 - import {SelectableBtn} from '../util/forms/SelectableBtn' 27 - import {Text} from '../util/text/Text' 28 - import * as Toast from '../util/Toast' 29 - import {ScrollView, TextInput} from './util' 30 - 31 - export const snapPoints = ['100%'] 32 - 33 - export type Props = {onChanged: () => void} 34 - 35 - export function Component(props: Props) { 36 - const {currentAccount} = useSession() 37 - const agent = useAgent() 38 - const { 39 - isLoading, 40 - data: serviceInfo, 41 - error: serviceInfoError, 42 - } = useServiceQuery(agent.service.toString()) 43 - 44 - return isLoading || !currentAccount ? ( 45 - <View style={{padding: 18}}> 46 - <ActivityIndicator /> 47 - </View> 48 - ) : serviceInfoError || !serviceInfo ? ( 49 - <ErrorMessage message={cleanError(serviceInfoError)} /> 50 - ) : ( 51 - <Inner 52 - {...props} 53 - currentAccount={currentAccount} 54 - serviceInfo={serviceInfo} 55 - /> 56 - ) 57 - } 58 - 59 - export function Inner({ 60 - currentAccount, 61 - serviceInfo, 62 - onChanged, 63 - }: Props & { 64 - currentAccount: SessionAccount 65 - serviceInfo: ComAtprotoServerDescribeServer.OutputSchema 66 - }) { 67 - const {_} = useLingui() 68 - const pal = usePalette('default') 69 - const {closeModal} = useModalControls() 70 - const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} = 71 - useUpdateHandleMutation() 72 - const agent = useAgent() 73 - 74 - const [error, setError] = useState<string>('') 75 - 76 - const [isCustom, setCustom] = React.useState<boolean>(false) 77 - const [handle, setHandle] = React.useState<string>('') 78 - const [canSave, setCanSave] = React.useState<boolean>(false) 79 - 80 - const userDomain = serviceInfo.availableUserDomains?.[0] 81 - 82 - // events 83 - // = 84 - const onPressCancel = React.useCallback(() => { 85 - closeModal() 86 - }, [closeModal]) 87 - const onToggleCustom = React.useCallback(() => { 88 - // toggle between a provided domain vs a custom one 89 - setHandle('') 90 - setCanSave(false) 91 - setCustom(!isCustom) 92 - }, [setCustom, isCustom]) 93 - const onPressSave = React.useCallback(async () => { 94 - if (!userDomain) { 95 - logger.error(`ChangeHandle: userDomain is undefined`, { 96 - service: serviceInfo, 97 - }) 98 - setError(`The service you've selected has no domains configured.`) 99 - return 100 - } 101 - 102 - try { 103 - const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) 104 - logger.debug(`Updating handle to ${newHandle}`) 105 - await updateHandle({ 106 - handle: newHandle, 107 - }) 108 - await agent.resumeSession(agent.session!) 109 - closeModal() 110 - onChanged() 111 - } catch (err: any) { 112 - setError(cleanError(err)) 113 - logger.error('Failed to update handle', {handle, message: err}) 114 - } finally { 115 - } 116 - }, [ 117 - setError, 118 - handle, 119 - userDomain, 120 - isCustom, 121 - onChanged, 122 - closeModal, 123 - updateHandle, 124 - serviceInfo, 125 - agent, 126 - ]) 127 - 128 - // rendering 129 - // = 130 - return ( 131 - <View style={[s.flex1, pal.view]}> 132 - <View style={[styles.title, pal.border]}> 133 - <View style={styles.titleLeft}> 134 - <TouchableOpacity 135 - onPress={onPressCancel} 136 - accessibilityRole="button" 137 - accessibilityLabel={_(msg`Cancel change handle`)} 138 - accessibilityHint={_(msg`Exits handle change process`)} 139 - onAccessibilityEscape={onPressCancel}> 140 - <Text type="lg" style={pal.textLight}> 141 - <Trans>Cancel</Trans> 142 - </Text> 143 - </TouchableOpacity> 144 - </View> 145 - <Text 146 - type="2xl-bold" 147 - style={[styles.titleMiddle, pal.text]} 148 - numberOfLines={1}> 149 - <Trans>Change Handle</Trans> 150 - </Text> 151 - <View style={styles.titleRight}> 152 - {isUpdateHandlePending ? ( 153 - <ActivityIndicator /> 154 - ) : canSave ? ( 155 - <TouchableOpacity 156 - onPress={onPressSave} 157 - accessibilityRole="button" 158 - accessibilityLabel={_(msg`Save handle change`)} 159 - accessibilityHint={_(msg`Saves handle change to ${handle}`)}> 160 - <Text type="2xl-medium" style={pal.link}> 161 - <Trans>Save</Trans> 162 - </Text> 163 - </TouchableOpacity> 164 - ) : undefined} 165 - </View> 166 - </View> 167 - <ScrollView style={styles.inner}> 168 - {error !== '' && ( 169 - <View style={styles.errorContainer}> 170 - <ErrorMessage message={error} /> 171 - </View> 172 - )} 173 - 174 - {isCustom ? ( 175 - <CustomHandleForm 176 - currentAccount={currentAccount} 177 - handle={handle} 178 - isProcessing={isUpdateHandlePending} 179 - canSave={canSave} 180 - onToggleCustom={onToggleCustom} 181 - setHandle={setHandle} 182 - setCanSave={setCanSave} 183 - onPressSave={onPressSave} 184 - /> 185 - ) : ( 186 - <ProvidedHandleForm 187 - handle={handle} 188 - userDomain={userDomain} 189 - isProcessing={isUpdateHandlePending} 190 - onToggleCustom={onToggleCustom} 191 - setHandle={setHandle} 192 - setCanSave={setCanSave} 193 - /> 194 - )} 195 - </ScrollView> 196 - </View> 197 - ) 198 - } 199 - 200 - /** 201 - * The form for using a domain allocated by the PDS 202 - */ 203 - function ProvidedHandleForm({ 204 - userDomain, 205 - handle, 206 - isProcessing, 207 - setHandle, 208 - onToggleCustom, 209 - setCanSave, 210 - }: { 211 - userDomain: string 212 - handle: string 213 - isProcessing: boolean 214 - setHandle: (v: string) => void 215 - onToggleCustom: () => void 216 - setCanSave: (v: boolean) => void 217 - }) { 218 - const pal = usePalette('default') 219 - const theme = useTheme() 220 - const {_} = useLingui() 221 - 222 - // events 223 - // = 224 - const onChangeHandle = React.useCallback( 225 - (v: string) => { 226 - const newHandle = makeValidHandle(v) 227 - setHandle(newHandle) 228 - setCanSave(newHandle.length > 0) 229 - }, 230 - [setHandle, setCanSave], 231 - ) 232 - 233 - // rendering 234 - // = 235 - return ( 236 - <> 237 - <View style={[pal.btn, styles.textInputWrapper]}> 238 - <FontAwesomeIcon 239 - icon="at" 240 - style={[pal.textLight, styles.textInputIcon]} 241 - /> 242 - <TextInput 243 - testID="setHandleInput" 244 - style={[pal.text, styles.textInput]} 245 - placeholder={_(msg`e.g. alice`)} 246 - placeholderTextColor={pal.colors.textLight} 247 - autoCapitalize="none" 248 - keyboardAppearance={theme.colorScheme} 249 - value={handle} 250 - onChangeText={onChangeHandle} 251 - editable={!isProcessing} 252 - accessible={true} 253 - accessibilityLabel={_(msg`Handle`)} 254 - accessibilityHint={_(msg`Sets Bluesky username`)} 255 - /> 256 - </View> 257 - <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}> 258 - <Trans> 259 - Your full handle will be{' '} 260 - <Text type="md-bold" style={pal.textLight}> 261 - @{createFullHandle(handle, userDomain)} 262 - </Text> 263 - </Trans> 264 - </Text> 265 - <TouchableOpacity 266 - onPress={onToggleCustom} 267 - accessibilityRole="button" 268 - accessibilityLabel={_(msg`Hosting provider`)} 269 - accessibilityHint={_(msg`Opens modal for using custom domain`)}> 270 - <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}> 271 - <Trans>I have my own domain</Trans> 272 - </Text> 273 - </TouchableOpacity> 274 - </> 275 - ) 276 - } 277 - 278 - /** 279 - * The form for using a custom domain 280 - */ 281 - function CustomHandleForm({ 282 - currentAccount, 283 - handle, 284 - canSave, 285 - isProcessing, 286 - setHandle, 287 - onToggleCustom, 288 - onPressSave, 289 - setCanSave, 290 - }: { 291 - currentAccount: SessionAccount 292 - handle: string 293 - canSave: boolean 294 - isProcessing: boolean 295 - setHandle: (v: string) => void 296 - onToggleCustom: () => void 297 - onPressSave: () => void 298 - setCanSave: (v: boolean) => void 299 - }) { 300 - const pal = usePalette('default') 301 - const palSecondary = usePalette('secondary') 302 - const palError = usePalette('error') 303 - const theme = useTheme() 304 - const {_} = useLingui() 305 - const [isVerifying, setIsVerifying] = React.useState(false) 306 - const [error, setError] = React.useState<string>('') 307 - const [isDNSForm, setDNSForm] = React.useState<boolean>(true) 308 - const fetchDid = useFetchDid() 309 - // events 310 - // = 311 - const onPressCopy = React.useCallback(() => { 312 - setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did) 313 - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 314 - }, [currentAccount, isDNSForm, _]) 315 - const onChangeHandle = React.useCallback( 316 - (v: string) => { 317 - setHandle(v) 318 - setCanSave(false) 319 - }, 320 - [setHandle, setCanSave], 321 - ) 322 - const onPressVerify = React.useCallback(async () => { 323 - if (canSave) { 324 - onPressSave() 325 - } 326 - try { 327 - setIsVerifying(true) 328 - setError('') 329 - const did = await fetchDid(handle) 330 - if (did === currentAccount.did) { 331 - setCanSave(true) 332 - } else { 333 - setError(`Incorrect DID returned (got ${did})`) 334 - } 335 - } catch (err: any) { 336 - setError(cleanError(err)) 337 - logger.error('Failed to verify domain', {handle, error: err}) 338 - } finally { 339 - setIsVerifying(false) 340 - } 341 - }, [ 342 - handle, 343 - currentAccount, 344 - setIsVerifying, 345 - setCanSave, 346 - setError, 347 - canSave, 348 - onPressSave, 349 - fetchDid, 350 - ]) 351 - 352 - // rendering 353 - // = 354 - return ( 355 - <> 356 - <Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain"> 357 - <Trans>Enter the domain you want to use</Trans> 358 - </Text> 359 - <View style={[pal.btn, styles.textInputWrapper]}> 360 - <FontAwesomeIcon 361 - icon="at" 362 - style={[pal.textLight, styles.textInputIcon]} 363 - /> 364 - <TextInput 365 - testID="setHandleInput" 366 - style={[pal.text, styles.textInput]} 367 - placeholder={_(msg`e.g. alice.com`)} 368 - placeholderTextColor={pal.colors.textLight} 369 - autoCapitalize="none" 370 - keyboardAppearance={theme.colorScheme} 371 - value={handle} 372 - onChangeText={onChangeHandle} 373 - editable={!isProcessing} 374 - accessibilityLabelledBy="customDomain" 375 - accessibilityLabel={_(msg`Custom domain`)} 376 - accessibilityHint={_(msg`Input your preferred hosting provider`)} 377 - /> 378 - </View> 379 - <View style={styles.spacer} /> 380 - 381 - <View style={[styles.selectableBtns]}> 382 - <SelectableBtn 383 - selected={isDNSForm} 384 - label={_(msg`DNS Panel`)} 385 - left 386 - onSelect={() => setDNSForm(true)} 387 - accessibilityHint={_(msg`Use the DNS panel`)} 388 - style={s.flex1} 389 - /> 390 - <SelectableBtn 391 - selected={!isDNSForm} 392 - label={_(msg`No DNS Panel`)} 393 - right 394 - onSelect={() => setDNSForm(false)} 395 - accessibilityHint={_(msg`Use a file on your server`)} 396 - style={s.flex1} 397 - /> 398 - </View> 399 - <View style={styles.spacer} /> 400 - {isDNSForm ? ( 401 - <> 402 - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> 403 - <Trans>Add the following DNS record to your domain:</Trans> 404 - </Text> 405 - <View style={[styles.dnsTable, pal.btn]}> 406 - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> 407 - <Trans>Host:</Trans> 408 - </Text> 409 - <View style={[styles.dnsValue]}> 410 - <Text type="mono" style={[styles.monoText, pal.text]}> 411 - _atproto 412 - </Text> 413 - </View> 414 - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> 415 - <Trans>Type:</Trans> 416 - </Text> 417 - <View style={[styles.dnsValue]}> 418 - <Text type="mono" style={[styles.monoText, pal.text]}> 419 - TXT 420 - </Text> 421 - </View> 422 - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> 423 - <Trans>Value:</Trans> 424 - </Text> 425 - <View style={[styles.dnsValue]}> 426 - <Text type="mono" style={[styles.monoText, pal.text]}> 427 - did={currentAccount.did} 428 - </Text> 429 - </View> 430 - </View> 431 - <Text type="md" style={[pal.text, s.pt20, s.pl5]}> 432 - <Trans>This should create a domain record at:</Trans> 433 - </Text> 434 - <Text type="mono" style={[styles.monoText, pal.text, s.pt5, s.pl5]}> 435 - _atproto.{handle} 436 - </Text> 437 - </> 438 - ) : ( 439 - <> 440 - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> 441 - <Trans>Upload a text file to:</Trans> 442 - </Text> 443 - <View style={[styles.valueContainer, pal.btn]}> 444 - <View style={[styles.dnsValue]}> 445 - <Text type="mono" style={[styles.monoText, pal.text]}> 446 - https://{handle}/.well-known/atproto-did 447 - </Text> 448 - </View> 449 - </View> 450 - <View style={styles.spacer} /> 451 - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> 452 - <Trans>That contains the following:</Trans> 453 - </Text> 454 - <View style={[styles.valueContainer, pal.btn]}> 455 - <View style={[styles.dnsValue]}> 456 - <Text type="mono" style={[styles.monoText, pal.text]}> 457 - {currentAccount.did} 458 - </Text> 459 - </View> 460 - </View> 461 - </> 462 - )} 463 - 464 - <View style={styles.spacer} /> 465 - <Button type="default" style={[s.p20, s.mb10]} onPress={onPressCopy}> 466 - <Text type="xl" style={[pal.link, s.textCenter]}> 467 - <Trans> 468 - Copy {isDNSForm ? _(msg`Domain Value`) : _(msg`File Contents`)} 469 - </Trans> 470 - </Text> 471 - </Button> 472 - {canSave === true && ( 473 - <View style={[styles.message, palSecondary.view]}> 474 - <Text type="md-medium" style={palSecondary.text}> 475 - <Trans>Domain verified!</Trans> 476 - </Text> 477 - </View> 478 - )} 479 - {error ? ( 480 - <View style={[styles.message, palError.view]}> 481 - <Text type="md-medium" style={palError.text}> 482 - {error} 483 - </Text> 484 - </View> 485 - ) : null} 486 - <Button 487 - type="primary" 488 - style={[s.p20, isVerifying && styles.dimmed]} 489 - onPress={onPressVerify}> 490 - {isVerifying ? ( 491 - <ActivityIndicator color="white" /> 492 - ) : ( 493 - <Text type="xl-medium" style={[s.white, s.textCenter]}> 494 - {canSave 495 - ? _(msg`Update to ${handle}`) 496 - : isDNSForm 497 - ? _(msg`Verify DNS Record`) 498 - : _(msg`Verify Text File`)} 499 - </Text> 500 - )} 501 - </Button> 502 - <View style={styles.spacer} /> 503 - <TouchableOpacity 504 - onPress={onToggleCustom} 505 - accessibilityLabel={_(msg`Use default provider`)} 506 - accessibilityHint={_(msg`Use bsky.social as hosting provider`)}> 507 - <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}> 508 - <Trans>Nevermind, create a handle for me</Trans> 509 - </Text> 510 - </TouchableOpacity> 511 - </> 512 - ) 513 - } 514 - 515 - const styles = StyleSheet.create({ 516 - inner: { 517 - padding: 14, 518 - }, 519 - footer: { 520 - padding: 14, 521 - }, 522 - spacer: { 523 - height: 20, 524 - }, 525 - dimmed: { 526 - opacity: 0.7, 527 - }, 528 - 529 - selectableBtns: { 530 - flexDirection: 'row', 531 - }, 532 - 533 - title: { 534 - flexDirection: 'row', 535 - alignItems: 'center', 536 - paddingTop: 25, 537 - paddingHorizontal: 20, 538 - paddingBottom: 15, 539 - borderBottomWidth: 1, 540 - }, 541 - titleLeft: { 542 - width: 80, 543 - }, 544 - titleRight: { 545 - width: 80, 546 - flexDirection: 'row', 547 - justifyContent: 'flex-end', 548 - }, 549 - titleMiddle: { 550 - flex: 1, 551 - textAlign: 'center', 552 - fontSize: 21, 553 - }, 554 - 555 - textInputWrapper: { 556 - borderRadius: 8, 557 - flexDirection: 'row', 558 - alignItems: 'center', 559 - }, 560 - textInputIcon: { 561 - marginLeft: 12, 562 - }, 563 - textInput: { 564 - flex: 1, 565 - width: '100%', 566 - paddingVertical: 10, 567 - paddingHorizontal: 8, 568 - fontSize: 17, 569 - letterSpacing: 0.25, 570 - fontWeight: '400', 571 - borderRadius: 10, 572 - }, 573 - 574 - valueContainer: { 575 - borderRadius: 4, 576 - paddingVertical: 16, 577 - }, 578 - 579 - dnsTable: { 580 - borderRadius: 4, 581 - paddingTop: 2, 582 - paddingBottom: 16, 583 - }, 584 - dnsLabel: { 585 - paddingHorizontal: 14, 586 - paddingTop: 10, 587 - }, 588 - dnsValue: { 589 - paddingHorizontal: 14, 590 - borderRadius: 4, 591 - }, 592 - monoText: { 593 - fontSize: 18, 594 - lineHeight: 20, 595 - }, 596 - 597 - message: { 598 - paddingHorizontal: 12, 599 - paddingVertical: 10, 600 - borderRadius: 8, 601 - marginBottom: 10, 602 - }, 603 - 604 - btn: { 605 - flexDirection: 'row', 606 - alignItems: 'center', 607 - justifyContent: 'center', 608 - width: '100%', 609 - borderRadius: 32, 610 - padding: 10, 611 - marginBottom: 10, 612 - }, 613 - errorContainer: {marginBottom: 10}, 614 - })
-8
src/view/com/modals/Modal.tsx
··· 7 7 import {useModalControls, useModals} from '#/state/modals' 8 8 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 9 9 import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' 10 - import * as AddAppPassword from './AddAppPasswords' 11 10 import * as ChangeEmailModal from './ChangeEmail' 12 - import * as ChangeHandleModal from './ChangeHandle' 13 11 import * as ChangePasswordModal from './ChangePassword' 14 12 import * as CreateOrEditListModal from './CreateOrEditList' 15 13 import * as DeleteAccountModal from './DeleteAccount' ··· 69 67 } else if (activeModal?.name === 'delete-account') { 70 68 snapPoints = DeleteAccountModal.snapPoints 71 69 element = <DeleteAccountModal.Component /> 72 - } else if (activeModal?.name === 'change-handle') { 73 - snapPoints = ChangeHandleModal.snapPoints 74 - element = <ChangeHandleModal.Component {...activeModal} /> 75 70 } else if (activeModal?.name === 'invite-codes') { 76 71 snapPoints = InviteCodesModal.snapPoints 77 72 element = <InviteCodesModal.Component /> 78 - } else if (activeModal?.name === 'add-app-password') { 79 - snapPoints = AddAppPassword.snapPoints 80 - element = <AddAppPassword.Component /> 81 73 } else if (activeModal?.name === 'content-languages-settings') { 82 74 snapPoints = ContentLanguagesSettingsModal.snapPoints 83 75 element = <ContentLanguagesSettingsModal.Component />
-6
src/view/com/modals/Modal.web.tsx
··· 7 7 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 8 import type {Modal as ModalIface} from '#/state/modals' 9 9 import {useModalControls, useModals} from '#/state/modals' 10 - import * as AddAppPassword from './AddAppPasswords' 11 10 import * as ChangeEmailModal from './ChangeEmail' 12 - import * as ChangeHandleModal from './ChangeHandle' 13 11 import * as ChangePasswordModal from './ChangePassword' 14 12 import * as CreateOrEditListModal from './CreateOrEditList' 15 13 import * as CropImageModal from './CropImage.web' ··· 74 72 element = <CropImageModal.Component {...modal} /> 75 73 } else if (modal.name === 'delete-account') { 76 74 element = <DeleteAccountModal.Component /> 77 - } else if (modal.name === 'change-handle') { 78 - element = <ChangeHandleModal.Component {...modal} /> 79 75 } else if (modal.name === 'invite-codes') { 80 76 element = <InviteCodesModal.Component /> 81 - } else if (modal.name === 'add-app-password') { 82 - element = <AddAppPassword.Component /> 83 77 } else if (modal.name === 'content-languages-settings') { 84 78 element = <ContentLanguagesSettingsModal.Component /> 85 79 } else if (modal.name === 'post-languages-settings') {
-66
src/view/com/util/AccountDropdownBtn.tsx
··· 1 - import React from 'react' 2 - import {Pressable} from 'react-native' 3 - import { 4 - FontAwesomeIcon, 5 - FontAwesomeIconStyle, 6 - } from '@fortawesome/react-native-fontawesome' 7 - import {msg} from '@lingui/macro' 8 - import {useLingui} from '@lingui/react' 9 - 10 - import {usePalette} from '#/lib/hooks/usePalette' 11 - import {s} from '#/lib/styles' 12 - import {SessionAccount, useSessionApi} from '#/state/session' 13 - import {useDialogControl} from '#/components/Dialog' 14 - import * as Prompt from '#/components/Prompt' 15 - import * as Toast from '../../com/util/Toast' 16 - import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' 17 - 18 - export function AccountDropdownBtn({account}: {account: SessionAccount}) { 19 - const pal = usePalette('default') 20 - const {removeAccount} = useSessionApi() 21 - const removePromptControl = useDialogControl() 22 - const {_} = useLingui() 23 - 24 - const items: DropdownItem[] = [ 25 - { 26 - label: _(msg`Remove account`), 27 - onPress: removePromptControl.open, 28 - icon: { 29 - ios: { 30 - name: 'trash', 31 - }, 32 - android: 'ic_delete', 33 - web: ['far', 'trash-can'], 34 - }, 35 - }, 36 - ] 37 - return ( 38 - <> 39 - <Pressable accessibilityRole="button" style={s.pl10}> 40 - <NativeDropdown 41 - testID="accountSettingsDropdownBtn" 42 - items={items} 43 - accessibilityLabel={_(msg`Account options`)} 44 - accessibilityHint=""> 45 - <FontAwesomeIcon 46 - icon="ellipsis-h" 47 - style={pal.textLight as FontAwesomeIconStyle} 48 - /> 49 - </NativeDropdown> 50 - </Pressable> 51 - <Prompt.Basic 52 - control={removePromptControl} 53 - title={_(msg`Remove from quick access?`)} 54 - description={_( 55 - msg`This will remove @${account.handle} from the quick access list.`, 56 - )} 57 - onConfirm={() => { 58 - removeAccount(account) 59 - Toast.show(_(msg`Account removed from quick access`)) 60 - }} 61 - confirmButtonCta={_(msg`Remove`)} 62 - confirmButtonColor="negative" 63 - /> 64 - </> 65 - ) 66 - }
+3 -1
src/view/com/util/List.web.tsx
··· 448 448 onItemSeen: ((item: any) => void) | undefined 449 449 }): React.ReactNode { 450 450 const rowRef = React.useRef(null) 451 - const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined) 451 + const intersectionTimeout = React.useRef< 452 + ReturnType<typeof setTimeout> | undefined 453 + >(undefined) 452 454 453 455 const handleIntersection = useNonReactiveCallback( 454 456 (entries: IntersectionObserverEntry[]) => {
-157
src/view/screens/AccessibilitySettings.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 4 - import {useLingui} from '@lingui/react' 5 - import {useFocusEffect} from '@react-navigation/native' 6 - 7 - import {IS_INTERNAL} from '#/lib/app-info' 8 - import {usePalette} from '#/lib/hooks/usePalette' 9 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 11 - import {s} from '#/lib/styles' 12 - import {isNative} from '#/platform/detection' 13 - import { 14 - useAutoplayDisabled, 15 - useHapticsDisabled, 16 - useRequireAltTextEnabled, 17 - useSetAutoplayDisabled, 18 - useSetHapticsDisabled, 19 - useSetRequireAltTextEnabled, 20 - } from '#/state/preferences' 21 - import { 22 - useLargeAltBadgeEnabled, 23 - useSetLargeAltBadgeEnabled, 24 - } from '#/state/preferences/large-alt-badge' 25 - import {useSetMinimalShellMode} from '#/state/shell' 26 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 27 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 28 - import {Text} from '#/view/com/util/text/Text' 29 - import {ScrollView} from '#/view/com/util/Views' 30 - import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' 31 - import {atoms as a} from '#/alf' 32 - import * as Layout from '#/components/Layout' 33 - 34 - type Props = NativeStackScreenProps< 35 - CommonNavigatorParams, 36 - 'AccessibilitySettings' 37 - > 38 - export function AccessibilitySettingsScreen(props: Props) { 39 - return IS_INTERNAL ? ( 40 - <NewAccessibilitySettingsScreen {...props} /> 41 - ) : ( 42 - <LegacyAccessibilitySettingsScreen {...props} /> 43 - ) 44 - } 45 - 46 - function LegacyAccessibilitySettingsScreen({}: Props) { 47 - const pal = usePalette('default') 48 - const setMinimalShellMode = useSetMinimalShellMode() 49 - const {isMobile, isTabletOrMobile} = useWebMediaQueries() 50 - const {_} = useLingui() 51 - 52 - const requireAltTextEnabled = useRequireAltTextEnabled() 53 - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() 54 - const autoplayDisabled = useAutoplayDisabled() 55 - const setAutoplayDisabled = useSetAutoplayDisabled() 56 - const hapticsDisabled = useHapticsDisabled() 57 - const setHapticsDisabled = useSetHapticsDisabled() 58 - const largeAltBadgeEnabled = useLargeAltBadgeEnabled() 59 - const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() 60 - 61 - useFocusEffect( 62 - React.useCallback(() => { 63 - setMinimalShellMode(false) 64 - }, [setMinimalShellMode]), 65 - ) 66 - 67 - return ( 68 - <Layout.Screen testID="accessibilitySettingsScreen"> 69 - <SimpleViewHeader 70 - showBackButton={isTabletOrMobile} 71 - style={[ 72 - pal.border, 73 - a.border_b, 74 - !isMobile && { 75 - borderLeftWidth: StyleSheet.hairlineWidth, 76 - borderRightWidth: StyleSheet.hairlineWidth, 77 - }, 78 - ]}> 79 - <View style={a.flex_1}> 80 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 81 - <Trans>Accessibility Settings</Trans> 82 - </Text> 83 - </View> 84 - </SimpleViewHeader> 85 - <ScrollView 86 - // @ts-ignore web only -prf 87 - dataSet={{'stable-gutters': 1}} 88 - style={s.flex1} 89 - contentContainerStyle={[ 90 - s.flex1, 91 - {paddingBottom: 100}, 92 - isMobile && pal.viewLight, 93 - ]}> 94 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 95 - <Trans>Alt text</Trans> 96 - </Text> 97 - <View style={[pal.view, styles.toggleCard]}> 98 - <ToggleButton 99 - type="default-light" 100 - label={_(msg`Require alt text before posting`)} 101 - labelType="lg" 102 - isSelected={requireAltTextEnabled} 103 - onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} 104 - /> 105 - <ToggleButton 106 - type="default-light" 107 - label={_(msg`Display larger alt text badges`)} 108 - labelType="lg" 109 - isSelected={!!largeAltBadgeEnabled} 110 - onPress={() => setLargeAltBadgeEnabled(!largeAltBadgeEnabled)} 111 - /> 112 - </View> 113 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 114 - <Trans>Media</Trans> 115 - </Text> 116 - <View style={[pal.view, styles.toggleCard]}> 117 - <ToggleButton 118 - type="default-light" 119 - label={_(msg`Disable autoplay for videos and GIFs`)} 120 - labelType="lg" 121 - isSelected={autoplayDisabled} 122 - onPress={() => setAutoplayDisabled(!autoplayDisabled)} 123 - /> 124 - </View> 125 - {isNative && ( 126 - <> 127 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 128 - <Trans>Haptics</Trans> 129 - </Text> 130 - <View style={[pal.view, styles.toggleCard]}> 131 - <ToggleButton 132 - type="default-light" 133 - label={_(msg`Disable haptic feedback`)} 134 - labelType="lg" 135 - isSelected={hapticsDisabled} 136 - onPress={() => setHapticsDisabled(!hapticsDisabled)} 137 - /> 138 - </View> 139 - </> 140 - )} 141 - </ScrollView> 142 - </Layout.Screen> 143 - ) 144 - } 145 - 146 - const styles = StyleSheet.create({ 147 - heading: { 148 - paddingHorizontal: 18, 149 - paddingTop: 14, 150 - paddingBottom: 6, 151 - }, 152 - toggleCard: { 153 - paddingVertical: 8, 154 - paddingHorizontal: 6, 155 - marginBottom: 1, 156 - }, 157 - })
-375
src/view/screens/AppPasswords.tsx
··· 1 - import React from 'react' 2 - import { 3 - ActivityIndicator, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 8 - import {ScrollView} from 'react-native-gesture-handler' 9 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 10 - import {msg, Trans} from '@lingui/macro' 11 - import {useLingui} from '@lingui/react' 12 - import {useFocusEffect} from '@react-navigation/native' 13 - import {NativeStackScreenProps} from '@react-navigation/native-stack' 14 - 15 - import {IS_INTERNAL} from '#/lib/app-info' 16 - import {usePalette} from '#/lib/hooks/usePalette' 17 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 18 - import {CommonNavigatorParams} from '#/lib/routes/types' 19 - import {cleanError} from '#/lib/strings/errors' 20 - import {useModalControls} from '#/state/modals' 21 - import { 22 - useAppPasswordDeleteMutation, 23 - useAppPasswordsQuery, 24 - } from '#/state/queries/app-passwords' 25 - import {useSetMinimalShellMode} from '#/state/shell' 26 - import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 27 - import {Button} from '#/view/com/util/forms/Button' 28 - import {Text} from '#/view/com/util/text/Text' 29 - import * as Toast from '#/view/com/util/Toast' 30 - import {ViewHeader} from '#/view/com/util/ViewHeader' 31 - import {CenteredView} from '#/view/com/util/Views' 32 - import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords' 33 - import {atoms as a} from '#/alf' 34 - import {useDialogControl} from '#/components/Dialog' 35 - import * as Layout from '#/components/Layout' 36 - import * as Prompt from '#/components/Prompt' 37 - 38 - type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> 39 - export function AppPasswords(props: Props) { 40 - return IS_INTERNAL ? ( 41 - <NewAppPasswordsScreen {...props} /> 42 - ) : ( 43 - <Layout.Screen testID="AppPasswordsScreen"> 44 - <AppPasswordsInner /> 45 - </Layout.Screen> 46 - ) 47 - } 48 - 49 - function AppPasswordsInner() { 50 - const pal = usePalette('default') 51 - const {_} = useLingui() 52 - const setMinimalShellMode = useSetMinimalShellMode() 53 - const {isTabletOrDesktop} = useWebMediaQueries() 54 - const {openModal} = useModalControls() 55 - const {data: appPasswords, error} = useAppPasswordsQuery() 56 - 57 - useFocusEffect( 58 - React.useCallback(() => { 59 - setMinimalShellMode(false) 60 - }, [setMinimalShellMode]), 61 - ) 62 - 63 - const onAdd = React.useCallback(async () => { 64 - openModal({name: 'add-app-password'}) 65 - }, [openModal]) 66 - 67 - if (error) { 68 - return ( 69 - <CenteredView 70 - style={[ 71 - styles.container, 72 - isTabletOrDesktop && styles.containerDesktop, 73 - pal.view, 74 - pal.border, 75 - ]} 76 - testID="appPasswordsScreen"> 77 - <ErrorScreen 78 - title={_(msg`Oops!`)} 79 - message={_(msg`There was an issue with fetching your app passwords`)} 80 - details={cleanError(error)} 81 - /> 82 - </CenteredView> 83 - ) 84 - } 85 - 86 - // no app passwords (empty) state 87 - if (appPasswords?.length === 0) { 88 - return ( 89 - <CenteredView 90 - style={[ 91 - styles.container, 92 - isTabletOrDesktop && styles.containerDesktop, 93 - pal.view, 94 - pal.border, 95 - ]} 96 - testID="appPasswordsScreen"> 97 - <AppPasswordsHeader /> 98 - <View style={[styles.empty, pal.viewLight]}> 99 - <Text type="lg" style={[pal.text, styles.emptyText]}> 100 - <Trans> 101 - You have not created any app passwords yet. You can create one by 102 - pressing the button below. 103 - </Trans> 104 - </Text> 105 - </View> 106 - {!isTabletOrDesktop && <View style={styles.flex1} />} 107 - <View 108 - style={[ 109 - styles.btnContainer, 110 - isTabletOrDesktop && styles.btnContainerDesktop, 111 - ]}> 112 - <Button 113 - testID="appPasswordBtn" 114 - type="primary" 115 - label={_(msg`Add App Password`)} 116 - style={styles.btn} 117 - labelStyle={styles.btnLabel} 118 - onPress={onAdd} 119 - /> 120 - </View> 121 - </CenteredView> 122 - ) 123 - } 124 - 125 - if (appPasswords?.length) { 126 - // has app passwords 127 - return ( 128 - <CenteredView 129 - style={[ 130 - styles.container, 131 - isTabletOrDesktop && styles.containerDesktop, 132 - pal.view, 133 - pal.border, 134 - ]} 135 - testID="appPasswordsScreen"> 136 - <AppPasswordsHeader /> 137 - <ScrollView 138 - style={[ 139 - styles.scrollContainer, 140 - pal.border, 141 - !isTabletOrDesktop && styles.flex1, 142 - ]}> 143 - {appPasswords.map((password, i) => ( 144 - <AppPassword 145 - key={password.name} 146 - testID={`appPassword-${i}`} 147 - name={password.name} 148 - createdAt={password.createdAt} 149 - privileged={password.privileged} 150 - /> 151 - ))} 152 - {isTabletOrDesktop && ( 153 - <View style={[styles.btnContainer, styles.btnContainerDesktop]}> 154 - <Button 155 - testID="appPasswordBtn" 156 - type="primary" 157 - label={_(msg`Add App Password`)} 158 - style={styles.btn} 159 - labelStyle={styles.btnLabel} 160 - onPress={onAdd} 161 - /> 162 - </View> 163 - )} 164 - </ScrollView> 165 - {!isTabletOrDesktop && ( 166 - <View style={styles.btnContainer}> 167 - <Button 168 - testID="appPasswordBtn" 169 - type="primary" 170 - label={_(msg`Add App Password`)} 171 - style={styles.btn} 172 - labelStyle={styles.btnLabel} 173 - onPress={onAdd} 174 - /> 175 - </View> 176 - )} 177 - </CenteredView> 178 - ) 179 - } 180 - 181 - return ( 182 - <CenteredView 183 - style={[ 184 - styles.container, 185 - isTabletOrDesktop && styles.containerDesktop, 186 - pal.view, 187 - pal.border, 188 - ]} 189 - testID="appPasswordsScreen"> 190 - <ActivityIndicator /> 191 - </CenteredView> 192 - ) 193 - } 194 - 195 - function AppPasswordsHeader() { 196 - const {isTabletOrDesktop} = useWebMediaQueries() 197 - const pal = usePalette('default') 198 - const {_} = useLingui() 199 - return ( 200 - <> 201 - <ViewHeader title={_(msg`App Passwords`)} showOnDesktop /> 202 - <Text 203 - type="sm" 204 - style={[ 205 - styles.description, 206 - pal.text, 207 - isTabletOrDesktop && styles.descriptionDesktop, 208 - ]}> 209 - <Trans> 210 - Use app passwords to login to other Bluesky clients without giving 211 - full access to your account or password. 212 - </Trans> 213 - </Text> 214 - </> 215 - ) 216 - } 217 - 218 - function AppPassword({ 219 - testID, 220 - name, 221 - createdAt, 222 - privileged, 223 - }: { 224 - testID: string 225 - name: string 226 - createdAt: string 227 - privileged?: boolean 228 - }) { 229 - const pal = usePalette('default') 230 - const {_, i18n} = useLingui() 231 - const control = useDialogControl() 232 - const deleteMutation = useAppPasswordDeleteMutation() 233 - 234 - const onDelete = React.useCallback(async () => { 235 - await deleteMutation.mutateAsync({name}) 236 - Toast.show(_(msg`App password deleted`)) 237 - }, [deleteMutation, name, _]) 238 - 239 - const onPress = React.useCallback(() => { 240 - control.open() 241 - }, [control]) 242 - 243 - return ( 244 - <TouchableOpacity 245 - testID={testID} 246 - style={[styles.item, pal.border]} 247 - onPress={onPress} 248 - accessibilityRole="button" 249 - accessibilityLabel={_(msg`Delete app password`)} 250 - accessibilityHint=""> 251 - <View> 252 - <Text type="md-bold" style={pal.text}> 253 - {name} 254 - </Text> 255 - <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}> 256 - <Trans> 257 - Created{' '} 258 - {i18n.date(createdAt, { 259 - year: 'numeric', 260 - month: 'numeric', 261 - day: 'numeric', 262 - hour: '2-digit', 263 - minute: '2-digit', 264 - second: '2-digit', 265 - })} 266 - </Trans> 267 - </Text> 268 - {privileged && ( 269 - <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_xs]}> 270 - <FontAwesomeIcon 271 - icon="circle-exclamation" 272 - color={pal.colors.textLight} 273 - size={14} 274 - /> 275 - <Text type="md" style={pal.textLight}> 276 - <Trans>Allows access to direct messages</Trans> 277 - </Text> 278 - </View> 279 - )} 280 - </View> 281 - <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> 282 - 283 - <Prompt.Basic 284 - control={control} 285 - title={_(msg`Delete app password?`)} 286 - description={_( 287 - msg`Are you sure you want to delete the app password "${name}"?`, 288 - )} 289 - onConfirm={onDelete} 290 - confirmButtonCta={_(msg`Delete`)} 291 - confirmButtonColor="negative" 292 - /> 293 - </TouchableOpacity> 294 - ) 295 - } 296 - 297 - const styles = StyleSheet.create({ 298 - container: { 299 - flex: 1, 300 - paddingBottom: 100, 301 - }, 302 - containerDesktop: { 303 - borderLeftWidth: 1, 304 - borderRightWidth: 1, 305 - paddingBottom: 0, 306 - }, 307 - title: { 308 - textAlign: 'center', 309 - marginTop: 12, 310 - marginBottom: 12, 311 - }, 312 - description: { 313 - textAlign: 'center', 314 - paddingHorizontal: 20, 315 - marginBottom: 14, 316 - }, 317 - descriptionDesktop: { 318 - marginTop: 14, 319 - }, 320 - 321 - scrollContainer: { 322 - borderTopWidth: 1, 323 - marginTop: 4, 324 - marginBottom: 16, 325 - }, 326 - 327 - flex1: { 328 - flex: 1, 329 - }, 330 - empty: { 331 - paddingHorizontal: 20, 332 - paddingVertical: 20, 333 - borderRadius: 16, 334 - marginHorizontal: 24, 335 - marginTop: 10, 336 - }, 337 - emptyText: { 338 - textAlign: 'center', 339 - }, 340 - 341 - item: { 342 - flexDirection: 'row', 343 - alignItems: 'center', 344 - justifyContent: 'space-between', 345 - borderBottomWidth: 1, 346 - paddingHorizontal: 20, 347 - paddingVertical: 14, 348 - }, 349 - pr10: { 350 - marginRight: 10, 351 - }, 352 - btnContainer: { 353 - flexDirection: 'row', 354 - justifyContent: 'center', 355 - }, 356 - btnContainerDesktop: { 357 - marginTop: 14, 358 - }, 359 - btn: { 360 - flexDirection: 'row', 361 - alignItems: 'center', 362 - justifyContent: 'center', 363 - borderRadius: 32, 364 - paddingHorizontal: 60, 365 - paddingVertical: 14, 366 - }, 367 - btnLabel: { 368 - fontSize: 18, 369 - }, 370 - 371 - trashIcon: { 372 - color: 'red', 373 - minWidth: 16, 374 - }, 375 - })
-336
src/view/screens/LanguageSettings.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' 4 - import { 5 - FontAwesomeIcon, 6 - FontAwesomeIconStyle, 7 - } from '@fortawesome/react-native-fontawesome' 8 - import {msg, Trans} from '@lingui/macro' 9 - import {useLingui} from '@lingui/react' 10 - import {useFocusEffect} from '@react-navigation/native' 11 - 12 - import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' 13 - import {IS_INTERNAL} from '#/lib/app-info' 14 - import {usePalette} from '#/lib/hooks/usePalette' 15 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 16 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 17 - import {s} from '#/lib/styles' 18 - import {sanitizeAppLanguageSetting} from '#/locale/helpers' 19 - import {useModalControls} from '#/state/modals' 20 - import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 21 - import {useSetMinimalShellMode} from '#/state/shell' 22 - import {Button} from '#/view/com/util/forms/Button' 23 - import {Text} from '#/view/com/util/text/Text' 24 - import {ViewHeader} from '#/view/com/util/ViewHeader' 25 - import {CenteredView} from '#/view/com/util/Views' 26 - import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' 27 - import * as Layout from '#/components/Layout' 28 - 29 - type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 30 - 31 - export function LanguageSettingsScreen(props: Props) { 32 - return IS_INTERNAL ? ( 33 - <NewLanguageSettingsScreen {...props} /> 34 - ) : ( 35 - <LegacyLanguageSettingsScreen {...props} /> 36 - ) 37 - } 38 - 39 - function LegacyLanguageSettingsScreen(_props: Props) { 40 - const pal = usePalette('default') 41 - const {_} = useLingui() 42 - const langPrefs = useLanguagePrefs() 43 - const setLangPrefs = useLanguagePrefsApi() 44 - const {isTabletOrDesktop} = useWebMediaQueries() 45 - const setMinimalShellMode = useSetMinimalShellMode() 46 - const {openModal} = useModalControls() 47 - 48 - useFocusEffect( 49 - React.useCallback(() => { 50 - setMinimalShellMode(false) 51 - }, [setMinimalShellMode]), 52 - ) 53 - 54 - const onPressContentLanguages = React.useCallback(() => { 55 - openModal({name: 'content-languages-settings'}) 56 - }, [openModal]) 57 - 58 - const onChangePrimaryLanguage = React.useCallback( 59 - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 60 - if (!value) return 61 - if (langPrefs.primaryLanguage !== value) { 62 - setLangPrefs.setPrimaryLanguage(value) 63 - } 64 - }, 65 - [langPrefs, setLangPrefs], 66 - ) 67 - 68 - const onChangeAppLanguage = React.useCallback( 69 - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 70 - if (!value) return 71 - if (langPrefs.appLanguage !== value) { 72 - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 73 - } 74 - }, 75 - [langPrefs, setLangPrefs], 76 - ) 77 - 78 - const myLanguages = React.useMemo(() => { 79 - return ( 80 - langPrefs.contentLanguages 81 - .map(lang => LANGUAGES.find(l => l.code2 === lang)) 82 - .filter(Boolean) 83 - // @ts-ignore 84 - .map(l => l.name) 85 - .join(', ') 86 - ) 87 - }, [langPrefs.contentLanguages]) 88 - 89 - return ( 90 - <Layout.Screen testID="PreferencesLanguagesScreen"> 91 - <CenteredView 92 - style={[ 93 - pal.view, 94 - pal.border, 95 - styles.container, 96 - isTabletOrDesktop && styles.desktopContainer, 97 - ]}> 98 - <ViewHeader title={_(msg`Language Settings`)} showOnDesktop /> 99 - 100 - <View style={{paddingTop: 20, paddingHorizontal: 20}}> 101 - {/* APP LANGUAGE */} 102 - <View style={{paddingBottom: 20}}> 103 - <Text type="title-sm" style={[pal.text, s.pb5]}> 104 - <Trans>App Language</Trans> 105 - </Text> 106 - <Text style={[pal.text, s.pb10]}> 107 - <Trans> 108 - Select your app language for the default text to display in the 109 - app. 110 - </Trans> 111 - </Text> 112 - 113 - <View style={{position: 'relative'}}> 114 - <RNPickerSelect 115 - placeholder={{}} 116 - value={sanitizeAppLanguageSetting(langPrefs.appLanguage)} 117 - onValueChange={onChangeAppLanguage} 118 - items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 119 - label: l.name, 120 - value: l.code2, 121 - key: l.code2, 122 - }))} 123 - style={{ 124 - inputAndroid: { 125 - backgroundColor: pal.viewLight.backgroundColor, 126 - color: pal.text.color, 127 - fontSize: 14, 128 - letterSpacing: 0.5, 129 - fontWeight: '600', 130 - paddingHorizontal: 14, 131 - paddingVertical: 8, 132 - borderRadius: 24, 133 - }, 134 - inputIOS: { 135 - backgroundColor: pal.viewLight.backgroundColor, 136 - color: pal.text.color, 137 - fontSize: 14, 138 - letterSpacing: 0.5, 139 - fontWeight: '600', 140 - paddingHorizontal: 14, 141 - paddingVertical: 8, 142 - borderRadius: 24, 143 - }, 144 - 145 - inputWeb: { 146 - cursor: 'pointer', 147 - // @ts-ignore web only 148 - '-moz-appearance': 'none', 149 - '-webkit-appearance': 'none', 150 - appearance: 'none', 151 - outline: 0, 152 - borderWidth: 0, 153 - backgroundColor: pal.viewLight.backgroundColor, 154 - color: pal.text.color, 155 - fontSize: 14, 156 - fontFamily: 'inherit', 157 - letterSpacing: 0.5, 158 - fontWeight: '600', 159 - paddingHorizontal: 14, 160 - paddingVertical: 8, 161 - borderRadius: 24, 162 - }, 163 - }} 164 - /> 165 - 166 - <View 167 - style={{ 168 - position: 'absolute', 169 - top: 1, 170 - right: 1, 171 - bottom: 1, 172 - width: 40, 173 - backgroundColor: pal.viewLight.backgroundColor, 174 - borderRadius: 24, 175 - pointerEvents: 'none', 176 - alignItems: 'center', 177 - justifyContent: 'center', 178 - }}> 179 - <FontAwesomeIcon 180 - icon="chevron-down" 181 - style={pal.text as FontAwesomeIconStyle} 182 - /> 183 - </View> 184 - </View> 185 - </View> 186 - 187 - <View 188 - style={{ 189 - height: 1, 190 - backgroundColor: pal.border.borderColor, 191 - marginBottom: 20, 192 - }} 193 - /> 194 - 195 - {/* PRIMARY LANGUAGE */} 196 - <View style={{paddingBottom: 20}}> 197 - <Text type="title-sm" style={[pal.text, s.pb5]}> 198 - <Trans>Primary Language</Trans> 199 - </Text> 200 - <Text style={[pal.text, s.pb10]}> 201 - <Trans> 202 - Select your preferred language for translations in your feed. 203 - </Trans> 204 - </Text> 205 - 206 - <View style={{position: 'relative'}}> 207 - <RNPickerSelect 208 - placeholder={{}} 209 - value={langPrefs.primaryLanguage} 210 - onValueChange={onChangePrimaryLanguage} 211 - items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 212 - label: l.name, 213 - value: l.code2, 214 - key: l.code2 + l.code3, 215 - }))} 216 - style={{ 217 - inputAndroid: { 218 - backgroundColor: pal.viewLight.backgroundColor, 219 - color: pal.text.color, 220 - fontSize: 14, 221 - letterSpacing: 0.5, 222 - fontWeight: '600', 223 - paddingHorizontal: 14, 224 - paddingVertical: 8, 225 - borderRadius: 24, 226 - }, 227 - inputIOS: { 228 - backgroundColor: pal.viewLight.backgroundColor, 229 - color: pal.text.color, 230 - fontSize: 14, 231 - letterSpacing: 0.5, 232 - fontWeight: '600', 233 - paddingHorizontal: 14, 234 - paddingVertical: 8, 235 - borderRadius: 24, 236 - }, 237 - inputWeb: { 238 - cursor: 'pointer', 239 - // @ts-ignore web only 240 - '-moz-appearance': 'none', 241 - '-webkit-appearance': 'none', 242 - appearance: 'none', 243 - outline: 0, 244 - borderWidth: 0, 245 - backgroundColor: pal.viewLight.backgroundColor, 246 - color: pal.text.color, 247 - fontSize: 14, 248 - fontFamily: 'inherit', 249 - letterSpacing: 0.5, 250 - fontWeight: '600', 251 - paddingHorizontal: 14, 252 - paddingVertical: 8, 253 - borderRadius: 24, 254 - }, 255 - }} 256 - /> 257 - 258 - <View 259 - style={{ 260 - position: 'absolute', 261 - top: 1, 262 - right: 1, 263 - bottom: 1, 264 - width: 40, 265 - backgroundColor: pal.viewLight.backgroundColor, 266 - borderRadius: 24, 267 - pointerEvents: 'none', 268 - alignItems: 'center', 269 - justifyContent: 'center', 270 - }}> 271 - <FontAwesomeIcon 272 - icon="chevron-down" 273 - style={pal.text as FontAwesomeIconStyle} 274 - /> 275 - </View> 276 - </View> 277 - </View> 278 - 279 - <View 280 - style={{ 281 - height: 1, 282 - backgroundColor: pal.border.borderColor, 283 - marginBottom: 20, 284 - }} 285 - /> 286 - 287 - {/* CONTENT LANGUAGES */} 288 - <View style={{paddingBottom: 20}}> 289 - <Text type="title-sm" style={[pal.text, s.pb5]}> 290 - <Trans>Content Languages</Trans> 291 - </Text> 292 - <Text style={[pal.text, s.pb10]}> 293 - <Trans> 294 - Select which languages you want your subscribed feeds to 295 - include. If none are selected, all languages will be shown. 296 - </Trans> 297 - </Text> 298 - 299 - <Button 300 - type="default" 301 - onPress={onPressContentLanguages} 302 - style={styles.button}> 303 - <FontAwesomeIcon 304 - icon={myLanguages.length ? 'check' : 'plus'} 305 - style={pal.text as FontAwesomeIconStyle} 306 - /> 307 - <Text 308 - type="button" 309 - style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]} 310 - numberOfLines={1}> 311 - {myLanguages.length ? myLanguages : _(msg`Select languages`)} 312 - </Text> 313 - </Button> 314 - </View> 315 - </View> 316 - </CenteredView> 317 - </Layout.Screen> 318 - ) 319 - } 320 - 321 - const styles = StyleSheet.create({ 322 - container: { 323 - flex: 1, 324 - paddingBottom: 90, 325 - }, 326 - desktopContainer: { 327 - borderLeftWidth: 1, 328 - borderRightWidth: 1, 329 - paddingBottom: 40, 330 - }, 331 - button: { 332 - flexDirection: 'row', 333 - alignItems: 'center', 334 - gap: 8, 335 - }, 336 - })
-147
src/view/screens/PreferencesExternalEmbeds.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {Trans} from '@lingui/macro' 4 - import {useFocusEffect} from '@react-navigation/native' 5 - 6 - import {IS_INTERNAL} from '#/lib/app-info' 7 - import {usePalette} from '#/lib/hooks/usePalette' 8 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 9 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 10 - import { 11 - EmbedPlayerSource, 12 - externalEmbedLabels, 13 - } from '#/lib/strings/embed-player' 14 - import { 15 - useExternalEmbedsPrefs, 16 - useSetExternalEmbedPref, 17 - } from '#/state/preferences' 18 - import {useSetMinimalShellMode} from '#/state/shell' 19 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 20 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 21 - import {Text} from '#/view/com/util/text/Text' 22 - import {ScrollView} from '#/view/com/util/Views' 23 - import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' 24 - import {atoms as a} from '#/alf' 25 - import * as Layout from '#/components/Layout' 26 - 27 - type Props = NativeStackScreenProps< 28 - CommonNavigatorParams, 29 - 'PreferencesExternalEmbeds' 30 - > 31 - export function PreferencesExternalEmbeds(props: Props) { 32 - return IS_INTERNAL ? ( 33 - <ExternalMediaPreferencesScreen {...props} /> 34 - ) : ( 35 - <LegacyPreferencesExternalEmbeds {...props} /> 36 - ) 37 - } 38 - 39 - function LegacyPreferencesExternalEmbeds({}: Props) { 40 - const pal = usePalette('default') 41 - const setMinimalShellMode = useSetMinimalShellMode() 42 - const {isTabletOrMobile} = useWebMediaQueries() 43 - 44 - useFocusEffect( 45 - React.useCallback(() => { 46 - setMinimalShellMode(false) 47 - }, [setMinimalShellMode]), 48 - ) 49 - 50 - return ( 51 - <Layout.Screen testID="preferencesExternalEmbedsScreen"> 52 - <ScrollView 53 - // @ts-ignore web only -prf 54 - dataSet={{'stable-gutters': 1}} 55 - contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}> 56 - <SimpleViewHeader 57 - showBackButton={isTabletOrMobile} 58 - style={[pal.border, a.border_b]}> 59 - <View style={a.flex_1}> 60 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 61 - <Trans>External Media Preferences</Trans> 62 - </Text> 63 - <Text style={pal.textLight}> 64 - <Trans>Customize media from external sites.</Trans> 65 - </Text> 66 - </View> 67 - </SimpleViewHeader> 68 - 69 - <View style={[pal.view]}> 70 - <View style={styles.infoCard}> 71 - <Text style={pal.text}> 72 - <Trans> 73 - External media may allow websites to collect information about 74 - you and your device. No information is sent or requested until 75 - you press the "play" button. 76 - </Trans> 77 - </Text> 78 - </View> 79 - </View> 80 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 81 - <Trans>Enable media players for</Trans> 82 - </Text> 83 - {Object.entries(externalEmbedLabels) 84 - // TODO: Remove special case when we disable the old integration. 85 - .filter(([key]) => key !== 'tenor') 86 - .map(([key, label]) => ( 87 - <PrefSelector 88 - source={key as EmbedPlayerSource} 89 - label={label} 90 - key={key} 91 - /> 92 - ))} 93 - </ScrollView> 94 - </Layout.Screen> 95 - ) 96 - } 97 - 98 - function PrefSelector({ 99 - source, 100 - label, 101 - }: { 102 - source: EmbedPlayerSource 103 - label: string 104 - }) { 105 - const pal = usePalette('default') 106 - const setExternalEmbedPref = useSetExternalEmbedPref() 107 - const sources = useExternalEmbedsPrefs() 108 - 109 - return ( 110 - <View> 111 - <View style={[pal.view, styles.toggleCard]}> 112 - <ToggleButton 113 - type="default-light" 114 - label={label} 115 - labelType="lg" 116 - isSelected={sources?.[source] === 'show'} 117 - onPress={() => 118 - setExternalEmbedPref( 119 - source, 120 - sources?.[source] === 'show' ? 'hide' : 'show', 121 - ) 122 - } 123 - /> 124 - </View> 125 - </View> 126 - ) 127 - } 128 - 129 - const styles = StyleSheet.create({ 130 - heading: { 131 - paddingHorizontal: 18, 132 - paddingTop: 14, 133 - paddingBottom: 14, 134 - }, 135 - spacer: { 136 - height: 8, 137 - }, 138 - infoCard: { 139 - paddingHorizontal: 20, 140 - paddingVertical: 14, 141 - }, 142 - toggleCard: { 143 - paddingVertical: 8, 144 - paddingHorizontal: 6, 145 - marginBottom: 1, 146 - }, 147 - })
-249
src/view/screens/PreferencesFollowingFeed.tsx
··· 1 - import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {IS_INTERNAL} from '#/lib/app-info' 8 - import {usePalette} from '#/lib/hooks/usePalette' 9 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 11 - import {colors, s} from '#/lib/styles' 12 - import { 13 - usePreferencesQuery, 14 - useSetFeedViewPreferencesMutation, 15 - } from '#/state/queries/preferences' 16 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 17 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 18 - import {Text} from '#/view/com/util/text/Text' 19 - import {ScrollView} from '#/view/com/util/Views' 20 - import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 21 - import {atoms as a} from '#/alf' 22 - import * as Layout from '#/components/Layout' 23 - 24 - type Props = NativeStackScreenProps< 25 - CommonNavigatorParams, 26 - 'PreferencesFollowingFeed' 27 - > 28 - export function PreferencesFollowingFeed(props: Props) { 29 - return IS_INTERNAL ? ( 30 - <FollowingFeedPreferencesScreen {...props} /> 31 - ) : ( 32 - <LegacyPreferencesFollowingFeed {...props} /> 33 - ) 34 - } 35 - 36 - function LegacyPreferencesFollowingFeed({}: Props) { 37 - const pal = usePalette('default') 38 - const {_} = useLingui() 39 - const {isTabletOrMobile} = useWebMediaQueries() 40 - const {data: preferences} = usePreferencesQuery() 41 - const {mutate: setFeedViewPref, variables} = 42 - useSetFeedViewPreferencesMutation() 43 - 44 - const showReplies = !( 45 - variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies 46 - ) 47 - 48 - return ( 49 - <Layout.Screen testID="preferencesHomeFeedScreen"> 50 - <ScrollView 51 - // @ts-ignore web only -sfn 52 - dataSet={{'stable-gutters': 1}} 53 - contentContainerStyle={{paddingBottom: 75}}> 54 - <SimpleViewHeader 55 - showBackButton={isTabletOrMobile} 56 - style={[pal.border, a.border_b]}> 57 - <View style={a.flex_1}> 58 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 59 - <Trans>Following Feed Preferences</Trans> 60 - </Text> 61 - <Text style={pal.textLight}> 62 - <Trans> 63 - Fine-tune the content you see on your Following feed. 64 - </Trans> 65 - </Text> 66 - </View> 67 - </SimpleViewHeader> 68 - <View style={styles.cardsContainer}> 69 - <View style={[pal.viewLight, styles.card]}> 70 - <Text type="title-sm" style={[pal.text, s.pb5]}> 71 - <Trans>Show Replies</Trans> 72 - </Text> 73 - <Text style={[pal.text, s.pb10]}> 74 - <Trans> 75 - Set this setting to "No" to hide all replies from your feed. 76 - </Trans> 77 - </Text> 78 - <ToggleButton 79 - testID="toggleRepliesBtn" 80 - type="default-light" 81 - label={showReplies ? _(msg`Yes`) : _(msg`No`)} 82 - isSelected={showReplies} 83 - onPress={() => 84 - setFeedViewPref({ 85 - hideReplies: !( 86 - variables?.hideReplies ?? 87 - preferences?.feedViewPrefs?.hideReplies 88 - ), 89 - }) 90 - } 91 - /> 92 - </View> 93 - <View style={[pal.viewLight, styles.card]}> 94 - <Text type="title-sm" style={[pal.text, s.pb5]}> 95 - <Trans>Show Reposts</Trans> 96 - </Text> 97 - <Text style={[pal.text, s.pb10]}> 98 - <Trans> 99 - Set this setting to "No" to hide all reposts from your feed. 100 - </Trans> 101 - </Text> 102 - <ToggleButton 103 - type="default-light" 104 - label={ 105 - variables?.hideReposts ?? 106 - preferences?.feedViewPrefs?.hideReposts 107 - ? _(msg`No`) 108 - : _(msg`Yes`) 109 - } 110 - isSelected={ 111 - !( 112 - variables?.hideReposts ?? 113 - preferences?.feedViewPrefs?.hideReposts 114 - ) 115 - } 116 - onPress={() => 117 - setFeedViewPref({ 118 - hideReposts: !( 119 - variables?.hideReposts ?? 120 - preferences?.feedViewPrefs?.hideReposts 121 - ), 122 - }) 123 - } 124 - /> 125 - </View> 126 - 127 - <View style={[pal.viewLight, styles.card]}> 128 - <Text type="title-sm" style={[pal.text, s.pb5]}> 129 - <Trans>Show Quote Posts</Trans> 130 - </Text> 131 - <Text style={[pal.text, s.pb10]}> 132 - <Trans> 133 - Set this setting to "No" to hide all quote posts from your feed. 134 - Reposts will still be visible. 135 - </Trans> 136 - </Text> 137 - <ToggleButton 138 - type="default-light" 139 - label={ 140 - variables?.hideQuotePosts ?? 141 - preferences?.feedViewPrefs?.hideQuotePosts 142 - ? _(msg`No`) 143 - : _(msg`Yes`) 144 - } 145 - isSelected={ 146 - !( 147 - variables?.hideQuotePosts ?? 148 - preferences?.feedViewPrefs?.hideQuotePosts 149 - ) 150 - } 151 - onPress={() => 152 - setFeedViewPref({ 153 - hideQuotePosts: !( 154 - variables?.hideQuotePosts ?? 155 - preferences?.feedViewPrefs?.hideQuotePosts 156 - ), 157 - }) 158 - } 159 - /> 160 - </View> 161 - 162 - <View style={[pal.viewLight, styles.card]}> 163 - <Text type="title-sm" style={[pal.text, s.pb5]}> 164 - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} 165 - <Trans>Show Posts from My Feeds</Trans> 166 - </Text> 167 - <Text style={[pal.text, s.pb10]}> 168 - <Trans> 169 - Set this setting to "Yes" to show samples of your saved feeds in 170 - your Following feed. This is an experimental feature. 171 - </Trans> 172 - </Text> 173 - <ToggleButton 174 - type="default-light" 175 - label={ 176 - variables?.lab_mergeFeedEnabled ?? 177 - preferences?.feedViewPrefs?.lab_mergeFeedEnabled 178 - ? _(msg`Yes`) 179 - : _(msg`No`) 180 - } 181 - isSelected={ 182 - !!( 183 - variables?.lab_mergeFeedEnabled ?? 184 - preferences?.feedViewPrefs?.lab_mergeFeedEnabled 185 - ) 186 - } 187 - onPress={() => 188 - setFeedViewPref({ 189 - lab_mergeFeedEnabled: !( 190 - variables?.lab_mergeFeedEnabled ?? 191 - preferences?.feedViewPrefs?.lab_mergeFeedEnabled 192 - ), 193 - }) 194 - } 195 - /> 196 - </View> 197 - </View> 198 - </ScrollView> 199 - </Layout.Screen> 200 - ) 201 - } 202 - 203 - const styles = StyleSheet.create({ 204 - container: { 205 - flex: 1, 206 - }, 207 - desktopContainer: { 208 - borderLeftWidth: 1, 209 - borderRightWidth: 1, 210 - }, 211 - titleSection: { 212 - paddingBottom: 30, 213 - }, 214 - title: { 215 - textAlign: 'center', 216 - marginBottom: 5, 217 - }, 218 - description: { 219 - textAlign: 'center', 220 - paddingHorizontal: 32, 221 - }, 222 - cardsContainer: { 223 - paddingHorizontal: 20, 224 - paddingVertical: 16, 225 - }, 226 - card: { 227 - padding: 16, 228 - borderRadius: 10, 229 - marginBottom: 20, 230 - }, 231 - btn: { 232 - flexDirection: 'row', 233 - alignItems: 'center', 234 - justifyContent: 'center', 235 - borderRadius: 32, 236 - padding: 14, 237 - backgroundColor: colors.blue3, 238 - }, 239 - btnDesktop: { 240 - marginHorizontal: 'auto', 241 - paddingHorizontal: 80, 242 - }, 243 - btnContainer: { 244 - paddingTop: 20, 245 - }, 246 - dimmed: { 247 - opacity: 0.3, 248 - }, 249 - })
-198
src/view/screens/PreferencesThreads.tsx
··· 1 - import React from 'react' 2 - import {ActivityIndicator, StyleSheet, View} from 'react-native' 3 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 - import {msg, Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 - 7 - import {IS_INTERNAL} from '#/lib/app-info' 8 - import {usePalette} from '#/lib/hooks/usePalette' 9 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 10 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 11 - import {colors, s} from '#/lib/styles' 12 - import { 13 - usePreferencesQuery, 14 - useSetThreadViewPreferencesMutation, 15 - } from '#/state/queries/preferences' 16 - import {RadioGroup} from '#/view/com/util/forms/RadioGroup' 17 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 18 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 19 - import {Text} from '#/view/com/util/text/Text' 20 - import {ScrollView} from '#/view/com/util/Views' 21 - import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' 22 - import {atoms as a} from '#/alf' 23 - import * as Layout from '#/components/Layout' 24 - 25 - type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> 26 - export function PreferencesThreads(props: Props) { 27 - return IS_INTERNAL ? ( 28 - <ThreadPreferencesScreen {...props} /> 29 - ) : ( 30 - <LegacyPreferencesThreads {...props} /> 31 - ) 32 - } 33 - 34 - function LegacyPreferencesThreads({}: Props) { 35 - const pal = usePalette('default') 36 - const {_} = useLingui() 37 - const {isTabletOrMobile} = useWebMediaQueries() 38 - const {data: preferences} = usePreferencesQuery() 39 - const {mutate: setThreadViewPrefs, variables} = 40 - useSetThreadViewPreferencesMutation() 41 - 42 - const prioritizeFollowedUsers = Boolean( 43 - variables?.prioritizeFollowedUsers ?? 44 - preferences?.threadViewPrefs?.prioritizeFollowedUsers, 45 - ) 46 - const treeViewEnabled = Boolean( 47 - variables?.lab_treeViewEnabled ?? 48 - preferences?.threadViewPrefs?.lab_treeViewEnabled, 49 - ) 50 - 51 - return ( 52 - <Layout.Screen testID="preferencesThreadsScreen"> 53 - <ScrollView 54 - // @ts-ignore web only -prf 55 - dataSet={{'stable-gutters': 1}} 56 - contentContainerStyle={{paddingBottom: 75}}> 57 - <SimpleViewHeader 58 - showBackButton={isTabletOrMobile} 59 - style={[pal.border, a.border_b]}> 60 - <View style={a.flex_1}> 61 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 62 - <Trans>Thread Preferences</Trans> 63 - </Text> 64 - <Text style={pal.textLight}> 65 - <Trans>Fine-tune the discussion threads.</Trans> 66 - </Text> 67 - </View> 68 - </SimpleViewHeader> 69 - 70 - {preferences ? ( 71 - <View style={styles.cardsContainer}> 72 - <View style={[pal.viewLight, styles.card]}> 73 - <Text type="title-sm" style={[pal.text, s.pb5]}> 74 - <Trans>Sort Replies</Trans> 75 - </Text> 76 - <Text style={[pal.text, s.pb10]}> 77 - <Trans>Sort replies to the same post by:</Trans> 78 - </Text> 79 - <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}> 80 - <RadioGroup 81 - type="default-light" 82 - items={[ 83 - {key: 'oldest', label: _(msg`Oldest replies first`)}, 84 - {key: 'newest', label: _(msg`Newest replies first`)}, 85 - { 86 - key: 'most-likes', 87 - label: _(msg`Most-liked replies first`), 88 - }, 89 - { 90 - key: 'random', 91 - label: _(msg`Random (aka "Poster's Roulette")`), 92 - }, 93 - ]} 94 - onSelect={key => setThreadViewPrefs({sort: key})} 95 - initialSelection={preferences?.threadViewPrefs?.sort} 96 - /> 97 - </View> 98 - </View> 99 - 100 - <View style={[pal.viewLight, styles.card]}> 101 - <Text type="title-sm" style={[pal.text, s.pb5]}> 102 - <Trans>Prioritize Your Follows</Trans> 103 - </Text> 104 - <Text style={[pal.text, s.pb10]}> 105 - <Trans> 106 - Show replies by people you follow before all other replies. 107 - </Trans> 108 - </Text> 109 - <ToggleButton 110 - type="default-light" 111 - label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)} 112 - isSelected={prioritizeFollowedUsers} 113 - onPress={() => 114 - setThreadViewPrefs({ 115 - prioritizeFollowedUsers: !prioritizeFollowedUsers, 116 - }) 117 - } 118 - /> 119 - </View> 120 - 121 - <View style={[pal.viewLight, styles.card]}> 122 - <Text type="title-sm" style={[pal.text, s.pb5]}> 123 - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} 124 - <Trans>Threaded Mode</Trans> 125 - </Text> 126 - <Text style={[pal.text, s.pb10]}> 127 - <Trans> 128 - Set this setting to "Yes" to show replies in a threaded view. 129 - This is an experimental feature. 130 - </Trans> 131 - </Text> 132 - <ToggleButton 133 - type="default-light" 134 - label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)} 135 - isSelected={treeViewEnabled} 136 - onPress={() => 137 - setThreadViewPrefs({ 138 - lab_treeViewEnabled: !treeViewEnabled, 139 - }) 140 - } 141 - /> 142 - </View> 143 - </View> 144 - ) : ( 145 - <ActivityIndicator style={a.flex_1} /> 146 - )} 147 - </ScrollView> 148 - </Layout.Screen> 149 - ) 150 - } 151 - 152 - const styles = StyleSheet.create({ 153 - container: { 154 - flex: 1, 155 - }, 156 - desktopContainer: { 157 - borderLeftWidth: 1, 158 - borderRightWidth: 1, 159 - }, 160 - titleSection: { 161 - paddingBottom: 30, 162 - }, 163 - title: { 164 - textAlign: 'center', 165 - marginBottom: 5, 166 - }, 167 - description: { 168 - textAlign: 'center', 169 - paddingHorizontal: 32, 170 - }, 171 - cardsContainer: { 172 - paddingHorizontal: 20, 173 - paddingVertical: 16, 174 - }, 175 - card: { 176 - padding: 16, 177 - borderRadius: 10, 178 - marginBottom: 20, 179 - }, 180 - btn: { 181 - flexDirection: 'row', 182 - alignItems: 'center', 183 - justifyContent: 'center', 184 - borderRadius: 32, 185 - padding: 14, 186 - backgroundColor: colors.blue3, 187 - }, 188 - btnDesktop: { 189 - marginHorizontal: 'auto', 190 - paddingHorizontal: 80, 191 - }, 192 - btnContainer: { 193 - paddingTop: 20, 194 - }, 195 - dimmed: { 196 - opacity: 0.3, 197 - }, 198 - })
src/view/screens/Settings/DisableEmail2FADialog.tsx src/screens/Settings/components/DisableEmail2FADialog.tsx
-58
src/view/screens/Settings/Email2FAToggle.tsx
··· 1 - import React from 'react' 2 - import {msg} from '@lingui/macro' 3 - import {useLingui} from '@lingui/react' 4 - 5 - import {useModalControls} from '#/state/modals' 6 - import {useAgent, useSession} from '#/state/session' 7 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 8 - import {useDialogControl} from '#/components/Dialog' 9 - import {DisableEmail2FADialog} from './DisableEmail2FADialog' 10 - 11 - export function Email2FAToggle() { 12 - const {_} = useLingui() 13 - const {currentAccount} = useSession() 14 - const {openModal} = useModalControls() 15 - const disableDialogCtrl = useDialogControl() 16 - const agent = useAgent() 17 - 18 - const enableEmailAuthFactor = React.useCallback(async () => { 19 - if (currentAccount?.email) { 20 - await agent.com.atproto.server.updateEmail({ 21 - email: currentAccount.email, 22 - emailAuthFactor: true, 23 - }) 24 - await agent.resumeSession(agent.session!) 25 - } 26 - }, [currentAccount, agent]) 27 - 28 - const onToggle = React.useCallback(() => { 29 - if (!currentAccount) { 30 - return 31 - } 32 - if (currentAccount.emailAuthFactor) { 33 - disableDialogCtrl.open() 34 - } else { 35 - if (!currentAccount.emailConfirmed) { 36 - openModal({ 37 - name: 'verify-email', 38 - onSuccess: enableEmailAuthFactor, 39 - }) 40 - return 41 - } 42 - enableEmailAuthFactor() 43 - } 44 - }, [currentAccount, enableEmailAuthFactor, openModal, disableDialogCtrl]) 45 - 46 - return ( 47 - <> 48 - <DisableEmail2FADialog control={disableDialogCtrl} /> 49 - <ToggleButton 50 - type="default-light" 51 - label={_(msg`Require email code to log into your account`)} 52 - labelType="lg" 53 - isSelected={!!currentAccount?.emailAuthFactor} 54 - onPress={onToggle} 55 - /> 56 - </> 57 - ) 58 - }
src/view/screens/Settings/ExportCarDialog.tsx src/screens/Settings/components/ExportCarDialog.tsx
-1077
src/view/screens/Settings/index.tsx
··· 1 - import React from 'react' 2 - import { 3 - Platform, 4 - Pressable, 5 - StyleSheet, 6 - TextStyle, 7 - TouchableOpacity, 8 - View, 9 - ViewStyle, 10 - } from 'react-native' 11 - import {setStringAsync} from 'expo-clipboard' 12 - import { 13 - FontAwesomeIcon, 14 - FontAwesomeIconStyle, 15 - } from '@fortawesome/react-native-fontawesome' 16 - import {msg, Trans} from '@lingui/macro' 17 - import {useLingui} from '@lingui/react' 18 - import {useFocusEffect, useNavigation} from '@react-navigation/native' 19 - import {useQueryClient} from '@tanstack/react-query' 20 - 21 - import {appVersion, BUNDLE_DATE, bundleInfo, IS_INTERNAL} from '#/lib/app-info' 22 - import {STATUS_PAGE_URL} from '#/lib/constants' 23 - import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 24 - import {useCustomPalette} from '#/lib/hooks/useCustomPalette' 25 - import {usePalette} from '#/lib/hooks/usePalette' 26 - import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 27 - import {HandIcon, HashtagIcon} from '#/lib/icons' 28 - import {makeProfileLink} from '#/lib/routes/links' 29 - import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' 30 - import {NavigationProp} from '#/lib/routes/types' 31 - import {colors, s} from '#/lib/styles' 32 - import {isNative} from '#/platform/detection' 33 - import {useModalControls} from '#/state/modals' 34 - import {clearStorage} from '#/state/persisted' 35 - import { 36 - useInAppBrowser, 37 - useSetInAppBrowser, 38 - } from '#/state/preferences/in-app-browser' 39 - import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' 40 - import {useClearPreferencesMutation} from '#/state/queries/preferences' 41 - import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' 42 - import {useProfileQuery} from '#/state/queries/profile' 43 - import {SessionAccount, useSession, useSessionApi} from '#/state/session' 44 - import {useOnboardingDispatch, useSetMinimalShellMode} from '#/state/shell' 45 - import {useLoggedOutViewControls} from '#/state/shell/logged-out' 46 - import {useCloseAllActiveElements} from '#/state/util' 47 - import {AccountDropdownBtn} from '#/view/com/util/AccountDropdownBtn' 48 - import {ToggleButton} from '#/view/com/util/forms/ToggleButton' 49 - import {Link, TextLink} from '#/view/com/util/Link' 50 - import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' 51 - import {Text} from '#/view/com/util/text/Text' 52 - import * as Toast from '#/view/com/util/Toast' 53 - import {UserAvatar} from '#/view/com/util/UserAvatar' 54 - import {ScrollView} from '#/view/com/util/Views' 55 - import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' 56 - import {SettingsScreen as NewSettingsScreen} from '#/screens/Settings/Settings' 57 - import {atoms as a, useTheme} from '#/alf' 58 - import {useDialogControl} from '#/components/Dialog' 59 - import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' 60 - import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' 61 - import * as Layout from '#/components/Layout' 62 - import {Email2FAToggle} from './Email2FAToggle' 63 - import {ExportCarDialog} from './ExportCarDialog' 64 - 65 - function SettingsAccountCard({ 66 - account, 67 - pendingDid, 68 - onPressSwitchAccount, 69 - }: { 70 - account: SessionAccount 71 - pendingDid: string | null 72 - onPressSwitchAccount: ( 73 - account: SessionAccount, 74 - logContext: 'Settings', 75 - ) => void 76 - }) { 77 - const pal = usePalette('default') 78 - const {_} = useLingui() 79 - const t = useTheme() 80 - const {currentAccount} = useSession() 81 - const {data: profile} = useProfileQuery({did: account.did}) 82 - const isCurrentAccount = account.did === currentAccount?.did 83 - 84 - const contents = ( 85 - <View 86 - style={[ 87 - pal.view, 88 - styles.linkCard, 89 - account.did === pendingDid && t.atoms.bg_contrast_25, 90 - ]}> 91 - <View style={styles.avi}> 92 - <UserAvatar 93 - size={40} 94 - avatar={profile?.avatar} 95 - type={profile?.associated?.labeler ? 'labeler' : 'user'} 96 - /> 97 - </View> 98 - <View style={[s.flex1]}> 99 - <Text 100 - emoji 101 - type="md-bold" 102 - style={[pal.text, a.self_start]} 103 - numberOfLines={1}> 104 - {profile?.displayName || account.handle} 105 - </Text> 106 - <Text emoji type="sm" style={pal.textLight} numberOfLines={1}> 107 - {account.handle} 108 - </Text> 109 - </View> 110 - <AccountDropdownBtn account={account} /> 111 - </View> 112 - ) 113 - 114 - return isCurrentAccount ? ( 115 - <Link 116 - href={makeProfileLink({ 117 - did: currentAccount?.did, 118 - handle: currentAccount?.handle, 119 - })} 120 - title={_(msg`Your profile`)} 121 - noFeedback> 122 - {contents} 123 - </Link> 124 - ) : ( 125 - <TouchableOpacity 126 - testID={`switchToAccountBtn-${account.handle}`} 127 - key={account.did} 128 - onPress={ 129 - pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings') 130 - } 131 - accessibilityRole="button" 132 - accessibilityLabel={_(msg`Switch to ${account.handle}`)} 133 - accessibilityHint={_(msg`Switches the account you are logged in to`)} 134 - activeOpacity={0.8}> 135 - {contents} 136 - </TouchableOpacity> 137 - ) 138 - } 139 - 140 - type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> 141 - export function SettingsScreen(props: Props) { 142 - return IS_INTERNAL ? ( 143 - <NewSettingsScreen {...props} /> 144 - ) : ( 145 - <LegacySettingsScreen {...props} /> 146 - ) 147 - } 148 - 149 - function LegacySettingsScreen({}: Props) { 150 - const queryClient = useQueryClient() 151 - const pal = usePalette('default') 152 - const {_} = useLingui() 153 - const setMinimalShellMode = useSetMinimalShellMode() 154 - const inAppBrowserPref = useInAppBrowser() 155 - const setUseInAppBrowser = useSetInAppBrowser() 156 - const onboardingDispatch = useOnboardingDispatch() 157 - const navigation = useNavigation<NavigationProp>() 158 - const {isMobile} = useWebMediaQueries() 159 - const {openModal} = useModalControls() 160 - const {accounts, currentAccount} = useSession() 161 - const {mutate: clearPreferences} = useClearPreferencesMutation() 162 - const {setShowLoggedOut} = useLoggedOutViewControls() 163 - const {logoutEveryAccount} = useSessionApi() 164 - const closeAllActiveElements = useCloseAllActiveElements() 165 - const exportCarControl = useDialogControl() 166 - const birthdayControl = useDialogControl() 167 - const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() 168 - const isSwitchingAccounts = !!pendingDid 169 - 170 - // const primaryBg = useCustomPalette<ViewStyle>({ 171 - // light: {backgroundColor: colors.blue0}, 172 - // dark: {backgroundColor: colors.blue6}, 173 - // }) 174 - // const primaryText = useCustomPalette<TextStyle>({ 175 - // light: {color: colors.blue3}, 176 - // dark: {color: colors.blue2}, 177 - // }) 178 - 179 - const dangerBg = useCustomPalette<ViewStyle>({ 180 - light: {backgroundColor: colors.red1}, 181 - dark: {backgroundColor: colors.red7}, 182 - }) 183 - const dangerText = useCustomPalette<TextStyle>({ 184 - light: {color: colors.red4}, 185 - dark: {color: colors.red2}, 186 - }) 187 - 188 - useFocusEffect( 189 - React.useCallback(() => { 190 - setMinimalShellMode(false) 191 - }, [setMinimalShellMode]), 192 - ) 193 - 194 - const onPressAddAccount = React.useCallback(() => { 195 - setShowLoggedOut(true) 196 - closeAllActiveElements() 197 - }, [setShowLoggedOut, closeAllActiveElements]) 198 - 199 - const onPressChangeHandle = React.useCallback(() => { 200 - openModal({ 201 - name: 'change-handle', 202 - onChanged() { 203 - if (currentAccount) { 204 - // refresh my profile 205 - queryClient.invalidateQueries({ 206 - queryKey: RQKEY_PROFILE(currentAccount.did), 207 - }) 208 - } 209 - }, 210 - }) 211 - }, [queryClient, openModal, currentAccount]) 212 - 213 - const onPressExportRepository = React.useCallback(() => { 214 - exportCarControl.open() 215 - }, [exportCarControl]) 216 - 217 - const onPressLanguageSettings = React.useCallback(() => { 218 - navigation.navigate('LanguageSettings') 219 - }, [navigation]) 220 - 221 - const onPressDeleteAccount = React.useCallback(() => { 222 - openModal({name: 'delete-account'}) 223 - }, [openModal]) 224 - 225 - const onPressLogoutEveryAccount = React.useCallback(() => { 226 - logoutEveryAccount('Settings') 227 - }, [logoutEveryAccount]) 228 - 229 - const onPressResetPreferences = React.useCallback(async () => { 230 - clearPreferences() 231 - }, [clearPreferences]) 232 - 233 - const onPressResetOnboarding = React.useCallback(async () => { 234 - navigation.navigate('Home') 235 - onboardingDispatch({type: 'start'}) 236 - Toast.show(_(msg`Onboarding reset`)) 237 - }, [navigation, onboardingDispatch, _]) 238 - 239 - const onPressBuildInfo = React.useCallback(() => { 240 - setStringAsync( 241 - `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}`, 242 - ) 243 - Toast.show(_(msg`Copied build version to clipboard`)) 244 - }, [_]) 245 - 246 - const openFollowingFeedPreferences = React.useCallback(() => { 247 - navigation.navigate('PreferencesFollowingFeed') 248 - }, [navigation]) 249 - 250 - const openThreadsPreferences = React.useCallback(() => { 251 - navigation.navigate('PreferencesThreads') 252 - }, [navigation]) 253 - 254 - const onPressAppPasswords = React.useCallback(() => { 255 - navigation.navigate('AppPasswords') 256 - }, [navigation]) 257 - 258 - const onPressSystemLog = React.useCallback(() => { 259 - navigation.navigate('Log') 260 - }, [navigation]) 261 - 262 - const onPressStorybook = React.useCallback(() => { 263 - navigation.navigate('Debug') 264 - }, [navigation]) 265 - 266 - const onPressDebugModeration = React.useCallback(() => { 267 - navigation.navigate('DebugMod') 268 - }, [navigation]) 269 - 270 - const onPressSavedFeeds = React.useCallback(() => { 271 - navigation.navigate('SavedFeeds') 272 - }, [navigation]) 273 - 274 - const onPressAccessibilitySettings = React.useCallback(() => { 275 - navigation.navigate('AccessibilitySettings') 276 - }, [navigation]) 277 - 278 - const onPressAppearanceSettings = React.useCallback(() => { 279 - navigation.navigate('AppearanceSettings') 280 - }, [navigation]) 281 - 282 - const onPressBirthday = React.useCallback(() => { 283 - birthdayControl.open() 284 - }, [birthdayControl]) 285 - 286 - const clearAllStorage = React.useCallback(async () => { 287 - await clearStorage() 288 - Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) 289 - }, [_]) 290 - 291 - const deactivateAccountControl = useDialogControl() 292 - const onPressDeactivateAccount = React.useCallback(() => { 293 - deactivateAccountControl.open() 294 - }, [deactivateAccountControl]) 295 - 296 - const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() 297 - 298 - return ( 299 - <Layout.Screen testID="settingsScreen"> 300 - <ExportCarDialog control={exportCarControl} /> 301 - <BirthDateSettingsDialog control={birthdayControl} /> 302 - 303 - <SimpleViewHeader 304 - showBackButton={isMobile} 305 - style={[ 306 - pal.border, 307 - {borderBottomWidth: StyleSheet.hairlineWidth}, 308 - !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, 309 - ]}> 310 - <View style={{flex: 1}}> 311 - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> 312 - <Trans>Settings</Trans> 313 - </Text> 314 - </View> 315 - </SimpleViewHeader> 316 - <ScrollView 317 - style={[isMobile && pal.viewLight]} 318 - scrollIndicatorInsets={{right: 1}} 319 - // @ts-ignore web only -prf 320 - dataSet={{'stable-gutters': 1}}> 321 - <View style={styles.spacer20} /> 322 - {currentAccount ? ( 323 - <> 324 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 325 - <Trans>Account</Trans> 326 - </Text> 327 - <View style={[styles.infoLine]}> 328 - <Text type="lg-medium" style={pal.text}> 329 - <Trans>Email:</Trans>{' '} 330 - </Text> 331 - {currentAccount.emailConfirmed && ( 332 - <> 333 - <FontAwesomeIcon 334 - icon="check" 335 - size={10} 336 - style={{color: colors.green3, marginRight: 2}} 337 - /> 338 - </> 339 - )} 340 - <Text 341 - type="lg" 342 - numberOfLines={1} 343 - style={[ 344 - pal.text, 345 - {overflow: 'hidden', marginRight: 4, flex: 1}, 346 - ]}> 347 - {currentAccount.email || '(no email)'} 348 - </Text> 349 - <Link onPress={() => openModal({name: 'change-email'})}> 350 - <Text type="lg" style={pal.link}> 351 - <Trans context="action">Change</Trans> 352 - </Text> 353 - </Link> 354 - </View> 355 - <View style={[styles.infoLine]}> 356 - <Text type="lg-medium" style={pal.text}> 357 - <Trans>Birthday:</Trans>{' '} 358 - </Text> 359 - <Link onPress={onPressBirthday}> 360 - <Text type="lg" style={pal.link}> 361 - <Trans>Show</Trans> 362 - </Text> 363 - </Link> 364 - </View> 365 - <View style={styles.spacer20} /> 366 - 367 - {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} 368 - 369 - <View style={[s.flexRow, styles.heading]}> 370 - <Text type="xl-bold" style={pal.text} numberOfLines={1}> 371 - <Trans>Signed in as</Trans> 372 - </Text> 373 - <View style={s.flex1} /> 374 - </View> 375 - <View pointerEvents={pendingDid ? 'none' : 'auto'}> 376 - <SettingsAccountCard 377 - account={currentAccount} 378 - onPressSwitchAccount={onPressSwitchAccount} 379 - pendingDid={pendingDid} 380 - /> 381 - </View> 382 - </> 383 - ) : null} 384 - 385 - <View pointerEvents={pendingDid ? 'none' : 'auto'}> 386 - {accounts.length > 1 && ( 387 - <View style={[s.flexRow, styles.heading, a.mt_sm]}> 388 - <Text type="xl-bold" style={pal.text} numberOfLines={1}> 389 - <Trans>Other accounts</Trans> 390 - </Text> 391 - <View style={s.flex1} /> 392 - </View> 393 - )} 394 - 395 - {accounts 396 - .filter(a => a.did !== currentAccount?.did) 397 - .map(account => ( 398 - <SettingsAccountCard 399 - key={account.did} 400 - account={account} 401 - onPressSwitchAccount={onPressSwitchAccount} 402 - pendingDid={pendingDid} 403 - /> 404 - ))} 405 - 406 - <TouchableOpacity 407 - testID="switchToNewAccountBtn" 408 - style={[styles.linkCard, pal.view]} 409 - onPress={isSwitchingAccounts ? undefined : onPressAddAccount} 410 - accessibilityRole="button" 411 - accessibilityLabel={_(msg`Add account`)} 412 - accessibilityHint={_(msg`Create a new Bluesky account`)}> 413 - <View style={[styles.iconContainer, pal.btn]}> 414 - <FontAwesomeIcon 415 - icon="plus" 416 - style={pal.text as FontAwesomeIconStyle} 417 - /> 418 - </View> 419 - <Text type="lg" style={pal.text}> 420 - <Trans>Add account</Trans> 421 - </Text> 422 - </TouchableOpacity> 423 - 424 - <TouchableOpacity 425 - style={[styles.linkCard, pal.view]} 426 - onPress={ 427 - isSwitchingAccounts ? undefined : onPressLogoutEveryAccount 428 - } 429 - accessibilityRole="button" 430 - accessibilityLabel={_(msg`Sign out of all accounts`)} 431 - accessibilityHint={undefined}> 432 - <View style={[styles.iconContainer, pal.btn]}> 433 - <FontAwesomeIcon 434 - icon="arrow-right-from-bracket" 435 - style={pal.text as FontAwesomeIconStyle} 436 - /> 437 - </View> 438 - <Text type="lg" style={pal.text}> 439 - {accounts.length > 1 ? ( 440 - <Trans>Sign out of all accounts</Trans> 441 - ) : ( 442 - <Trans>Sign out</Trans> 443 - )} 444 - </Text> 445 - </TouchableOpacity> 446 - </View> 447 - 448 - <View style={styles.spacer20} /> 449 - 450 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 451 - <Trans>Basics</Trans> 452 - </Text> 453 - <TouchableOpacity 454 - testID="accessibilitySettingsBtn" 455 - style={[ 456 - styles.linkCard, 457 - pal.view, 458 - isSwitchingAccounts && styles.dimmed, 459 - ]} 460 - onPress={ 461 - isSwitchingAccounts ? undefined : onPressAccessibilitySettings 462 - } 463 - accessibilityRole="button" 464 - accessibilityLabel={_(msg`Accessibility settings`)} 465 - accessibilityHint={_(msg`Opens accessibility settings`)}> 466 - <View style={[styles.iconContainer, pal.btn]}> 467 - <FontAwesomeIcon 468 - icon="universal-access" 469 - style={pal.text as FontAwesomeIconStyle} 470 - /> 471 - </View> 472 - <Text type="lg" style={pal.text}> 473 - <Trans>Accessibility</Trans> 474 - </Text> 475 - </TouchableOpacity> 476 - <TouchableOpacity 477 - testID="appearanceSettingsBtn" 478 - style={[ 479 - styles.linkCard, 480 - pal.view, 481 - isSwitchingAccounts && styles.dimmed, 482 - ]} 483 - onPress={isSwitchingAccounts ? undefined : onPressAppearanceSettings} 484 - accessibilityRole="button" 485 - accessibilityLabel={_(msg`Appearance settings`)} 486 - accessibilityHint={_(msg`Opens appearance settings`)}> 487 - <View style={[styles.iconContainer, pal.btn]}> 488 - <FontAwesomeIcon 489 - icon="paint-roller" 490 - style={pal.text as FontAwesomeIconStyle} 491 - /> 492 - </View> 493 - <Text type="lg" style={pal.text}> 494 - <Trans>Appearance</Trans> 495 - </Text> 496 - </TouchableOpacity> 497 - <TouchableOpacity 498 - testID="languageSettingsBtn" 499 - style={[ 500 - styles.linkCard, 501 - pal.view, 502 - isSwitchingAccounts && styles.dimmed, 503 - ]} 504 - onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} 505 - accessibilityRole="button" 506 - accessibilityLabel={_(msg`Language settings`)} 507 - accessibilityHint={_(msg`Opens configurable language settings`)}> 508 - <View style={[styles.iconContainer, pal.btn]}> 509 - <FontAwesomeIcon 510 - icon="language" 511 - style={pal.text as FontAwesomeIconStyle} 512 - /> 513 - </View> 514 - <Text type="lg" style={pal.text}> 515 - <Trans>Languages</Trans> 516 - </Text> 517 - </TouchableOpacity> 518 - <TouchableOpacity 519 - testID="moderationBtn" 520 - style={[ 521 - styles.linkCard, 522 - pal.view, 523 - isSwitchingAccounts && styles.dimmed, 524 - ]} 525 - onPress={ 526 - isSwitchingAccounts 527 - ? undefined 528 - : () => navigation.navigate('Moderation') 529 - } 530 - accessibilityRole="button" 531 - accessibilityLabel={_(msg`Moderation settings`)} 532 - accessibilityHint={_(msg`Opens moderation settings`)}> 533 - <View style={[styles.iconContainer, pal.btn]}> 534 - <HandIcon style={pal.text} size={18} strokeWidth={6} /> 535 - </View> 536 - <Text type="lg" style={pal.text}> 537 - <Trans>Moderation</Trans> 538 - </Text> 539 - </TouchableOpacity> 540 - <TouchableOpacity 541 - testID="preferencesHomeFeedButton" 542 - style={[ 543 - styles.linkCard, 544 - pal.view, 545 - isSwitchingAccounts && styles.dimmed, 546 - ]} 547 - onPress={openFollowingFeedPreferences} 548 - accessibilityRole="button" 549 - accessibilityLabel={_(msg`Following feed preferences`)} 550 - accessibilityHint={_(msg`Opens the Following feed preferences`)}> 551 - <View style={[styles.iconContainer, pal.btn]}> 552 - <FontAwesomeIcon 553 - icon="sliders" 554 - style={pal.text as FontAwesomeIconStyle} 555 - /> 556 - </View> 557 - <Text type="lg" style={pal.text}> 558 - <Trans>Following Feed Preferences</Trans> 559 - </Text> 560 - </TouchableOpacity> 561 - <TouchableOpacity 562 - testID="preferencesThreadsButton" 563 - style={[ 564 - styles.linkCard, 565 - pal.view, 566 - isSwitchingAccounts && styles.dimmed, 567 - ]} 568 - onPress={openThreadsPreferences} 569 - accessibilityRole="button" 570 - accessibilityLabel={_(msg`Thread preferences`)} 571 - accessibilityHint={_(msg`Opens the threads preferences`)}> 572 - <View style={[styles.iconContainer, pal.btn]}> 573 - <FontAwesomeIcon 574 - icon={['far', 'comments']} 575 - style={pal.text as FontAwesomeIconStyle} 576 - size={18} 577 - /> 578 - </View> 579 - <Text type="lg" style={pal.text}> 580 - <Trans>Thread Preferences</Trans> 581 - </Text> 582 - </TouchableOpacity> 583 - <TouchableOpacity 584 - testID="savedFeedsBtn" 585 - style={[ 586 - styles.linkCard, 587 - pal.view, 588 - isSwitchingAccounts && styles.dimmed, 589 - ]} 590 - onPress={onPressSavedFeeds} 591 - accessibilityRole="button" 592 - accessibilityLabel={_(msg`My saved feeds`)} 593 - accessibilityHint={_(msg`Opens screen with all saved feeds`)}> 594 - <View style={[styles.iconContainer, pal.btn]}> 595 - <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> 596 - </View> 597 - <Text type="lg" style={pal.text}> 598 - <Trans>My Saved Feeds</Trans> 599 - </Text> 600 - </TouchableOpacity> 601 - <TouchableOpacity 602 - testID="linkToChatSettingsBtn" 603 - style={[ 604 - styles.linkCard, 605 - pal.view, 606 - isSwitchingAccounts && styles.dimmed, 607 - ]} 608 - onPress={ 609 - isSwitchingAccounts 610 - ? undefined 611 - : () => navigation.navigate('MessagesSettings') 612 - } 613 - accessibilityRole="button" 614 - accessibilityLabel={_(msg`Chat settings`)} 615 - accessibilityHint={_(msg`Opens chat settings`)}> 616 - <View style={[styles.iconContainer, pal.btn]}> 617 - <FontAwesomeIcon 618 - icon={['far', 'comment-dots']} 619 - style={pal.text as FontAwesomeIconStyle} 620 - /> 621 - </View> 622 - <Text type="lg" style={pal.text}> 623 - <Trans>Chat Settings</Trans> 624 - </Text> 625 - </TouchableOpacity> 626 - 627 - <View style={styles.spacer20} /> 628 - 629 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 630 - <Trans>Privacy</Trans> 631 - </Text> 632 - 633 - <TouchableOpacity 634 - testID="externalEmbedsBtn" 635 - style={[ 636 - styles.linkCard, 637 - pal.view, 638 - isSwitchingAccounts && styles.dimmed, 639 - ]} 640 - onPress={ 641 - isSwitchingAccounts 642 - ? undefined 643 - : () => navigation.navigate('PreferencesExternalEmbeds') 644 - } 645 - accessibilityRole="button" 646 - accessibilityLabel={_(msg`External media settings`)} 647 - accessibilityHint={_(msg`Opens external embeds settings`)}> 648 - <View style={[styles.iconContainer, pal.btn]}> 649 - <FontAwesomeIcon 650 - icon={['far', 'circle-play']} 651 - style={pal.text as FontAwesomeIconStyle} 652 - /> 653 - </View> 654 - <Text type="lg" style={pal.text}> 655 - <Trans>External Media Preferences</Trans> 656 - </Text> 657 - </TouchableOpacity> 658 - 659 - <View style={styles.spacer20} /> 660 - 661 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 662 - <Trans>Advanced</Trans> 663 - </Text> 664 - <TouchableOpacity 665 - testID="appPasswordBtn" 666 - style={[ 667 - styles.linkCard, 668 - pal.view, 669 - isSwitchingAccounts && styles.dimmed, 670 - ]} 671 - onPress={onPressAppPasswords} 672 - accessibilityRole="button" 673 - accessibilityLabel={_(msg`App password settings`)} 674 - accessibilityHint={_(msg`Opens the app password settings`)}> 675 - <View style={[styles.iconContainer, pal.btn]}> 676 - <FontAwesomeIcon 677 - icon="lock" 678 - style={pal.text as FontAwesomeIconStyle} 679 - /> 680 - </View> 681 - <Text type="lg" style={pal.text}> 682 - <Trans>App Passwords</Trans> 683 - </Text> 684 - </TouchableOpacity> 685 - <TouchableOpacity 686 - testID="changeHandleBtn" 687 - style={[ 688 - styles.linkCard, 689 - pal.view, 690 - isSwitchingAccounts && styles.dimmed, 691 - ]} 692 - onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} 693 - accessibilityRole="button" 694 - accessibilityLabel={_(msg`Change handle`)} 695 - accessibilityHint={_( 696 - msg`Opens modal for choosing a new Bluesky handle`, 697 - )}> 698 - <View style={[styles.iconContainer, pal.btn]}> 699 - <FontAwesomeIcon 700 - icon="at" 701 - style={pal.text as FontAwesomeIconStyle} 702 - /> 703 - </View> 704 - <Text type="lg" style={pal.text} numberOfLines={1}> 705 - <Trans>Change Handle</Trans> 706 - </Text> 707 - </TouchableOpacity> 708 - {isNative && ( 709 - <View style={[pal.view, styles.toggleCard]}> 710 - <ToggleButton 711 - type="default-light" 712 - label={_(msg`Open links with in-app browser`)} 713 - labelType="lg" 714 - isSelected={inAppBrowserPref ?? false} 715 - onPress={() => setUseInAppBrowser(!inAppBrowserPref)} 716 - /> 717 - </View> 718 - )} 719 - <View style={styles.spacer20} /> 720 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 721 - <Trans>Two-factor authentication</Trans> 722 - </Text> 723 - <View style={[pal.view, styles.toggleCard]}> 724 - <Email2FAToggle /> 725 - </View> 726 - <View style={styles.spacer20} /> 727 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 728 - <Trans>Account</Trans> 729 - </Text> 730 - <TouchableOpacity 731 - testID="changePasswordBtn" 732 - style={[ 733 - styles.linkCard, 734 - pal.view, 735 - isSwitchingAccounts && styles.dimmed, 736 - ]} 737 - onPress={() => openModal({name: 'change-password'})} 738 - accessibilityRole="button" 739 - accessibilityLabel={_(msg`Change password`)} 740 - accessibilityHint={_( 741 - msg`Opens modal for changing your Bluesky password`, 742 - )}> 743 - <View style={[styles.iconContainer, pal.btn]}> 744 - <FontAwesomeIcon 745 - icon="lock" 746 - style={pal.text as FontAwesomeIconStyle} 747 - /> 748 - </View> 749 - <Text type="lg" style={pal.text} numberOfLines={1}> 750 - <Trans>Change Password</Trans> 751 - </Text> 752 - </TouchableOpacity> 753 - <TouchableOpacity 754 - testID="exportRepositoryBtn" 755 - style={[ 756 - styles.linkCard, 757 - pal.view, 758 - isSwitchingAccounts && styles.dimmed, 759 - ]} 760 - onPress={isSwitchingAccounts ? undefined : onPressExportRepository} 761 - accessibilityRole="button" 762 - accessibilityLabel={_(msg`Export my data`)} 763 - accessibilityHint={_( 764 - msg`Opens modal for downloading your Bluesky account data (repository)`, 765 - )}> 766 - <View style={[styles.iconContainer, pal.btn]}> 767 - <FontAwesomeIcon 768 - icon="download" 769 - style={pal.text as FontAwesomeIconStyle} 770 - /> 771 - </View> 772 - <Text type="lg" style={pal.text} numberOfLines={1}> 773 - <Trans>Export My Data</Trans> 774 - </Text> 775 - </TouchableOpacity> 776 - 777 - <TouchableOpacity 778 - style={[pal.view, styles.linkCard]} 779 - onPress={onPressDeactivateAccount} 780 - accessible={true} 781 - accessibilityRole="button" 782 - accessibilityLabel={_(msg`Deactivate account`)} 783 - accessibilityHint={_( 784 - msg`Opens modal for account deactivation confirmation`, 785 - )}> 786 - <View style={[styles.iconContainer, dangerBg]}> 787 - <FontAwesomeIcon 788 - icon={'users-slash'} 789 - style={dangerText as FontAwesomeIconStyle} 790 - size={18} 791 - /> 792 - </View> 793 - <Text type="lg" style={dangerText}> 794 - <Trans>Deactivate my account</Trans> 795 - </Text> 796 - </TouchableOpacity> 797 - <DeactivateAccountDialog control={deactivateAccountControl} /> 798 - 799 - <TouchableOpacity 800 - style={[pal.view, styles.linkCard]} 801 - onPress={onPressDeleteAccount} 802 - accessible={true} 803 - accessibilityRole="button" 804 - accessibilityLabel={_(msg`Delete account`)} 805 - accessibilityHint={_( 806 - msg`Opens modal for account deletion confirmation. Requires email code`, 807 - )}> 808 - <View style={[styles.iconContainer, dangerBg]}> 809 - <FontAwesomeIcon 810 - icon={['far', 'trash-can']} 811 - style={dangerText as FontAwesomeIconStyle} 812 - size={18} 813 - /> 814 - </View> 815 - <Text type="lg" style={dangerText}> 816 - <Trans>Delete My Account…</Trans> 817 - </Text> 818 - </TouchableOpacity> 819 - <View style={styles.spacer20} /> 820 - <TouchableOpacity 821 - style={[pal.view, styles.linkCardNoIcon]} 822 - onPress={onPressSystemLog} 823 - accessibilityRole="button" 824 - accessibilityLabel={_(msg`Open system log`)} 825 - accessibilityHint={_(msg`Opens the system log page`)}> 826 - <Text type="lg" style={pal.text}> 827 - <Trans>System log</Trans> 828 - </Text> 829 - </TouchableOpacity> 830 - {__DEV__ ? ( 831 - <> 832 - <TouchableOpacity 833 - style={[pal.view, styles.linkCardNoIcon]} 834 - onPress={onPressStorybook} 835 - accessibilityRole="button" 836 - accessibilityLabel={_(msg`Open storybook page`)} 837 - accessibilityHint={_(msg`Opens the storybook page`)}> 838 - <Text type="lg" style={pal.text}> 839 - <Trans>Storybook</Trans> 840 - </Text> 841 - </TouchableOpacity> 842 - <TouchableOpacity 843 - style={[pal.view, styles.linkCardNoIcon]} 844 - onPress={onPressDebugModeration} 845 - accessibilityRole="button" 846 - accessibilityLabel={_(msg`Open storybook page`)} 847 - accessibilityHint={_(msg`Opens the storybook page`)}> 848 - <Text type="lg" style={pal.text}> 849 - <Trans>Debug Moderation</Trans> 850 - </Text> 851 - </TouchableOpacity> 852 - <TouchableOpacity 853 - style={[pal.view, styles.linkCardNoIcon]} 854 - onPress={onPressResetPreferences} 855 - accessibilityRole="button" 856 - accessibilityLabel={_(msg`Reset preferences state`)} 857 - accessibilityHint={_(msg`Resets the preferences state`)}> 858 - <Text type="lg" style={pal.text}> 859 - <Trans>Reset preferences state</Trans> 860 - </Text> 861 - </TouchableOpacity> 862 - <TouchableOpacity 863 - style={[pal.view, styles.linkCardNoIcon]} 864 - onPress={() => onPressDeleteChatDeclaration()} 865 - accessibilityRole="button" 866 - accessibilityLabel={_(msg`Delete chat declaration record`)} 867 - accessibilityHint={_(msg`Deletes the chat declaration record`)}> 868 - <Text type="lg" style={pal.text}> 869 - <Trans>Delete chat declaration record</Trans> 870 - </Text> 871 - </TouchableOpacity> 872 - <TouchableOpacity 873 - style={[pal.view, styles.linkCardNoIcon]} 874 - onPress={onPressResetOnboarding} 875 - accessibilityRole="button" 876 - accessibilityLabel={_(msg`Reset onboarding state`)} 877 - accessibilityHint={_(msg`Resets the onboarding state`)}> 878 - <Text type="lg" style={pal.text}> 879 - <Trans>Reset onboarding state</Trans> 880 - </Text> 881 - </TouchableOpacity> 882 - <TouchableOpacity 883 - style={[pal.view, styles.linkCardNoIcon]} 884 - onPress={clearAllStorage} 885 - accessibilityRole="button" 886 - accessibilityLabel={_(msg`Clear all storage data`)} 887 - accessibilityHint={_(msg`Clears all storage data`)}> 888 - <Text type="lg" style={pal.text}> 889 - <Trans>Clear all storage data (restart after this)</Trans> 890 - </Text> 891 - </TouchableOpacity> 892 - </> 893 - ) : null} 894 - <View style={[styles.footer]}> 895 - <TouchableOpacity 896 - accessibilityRole="button" 897 - onPress={onPressBuildInfo}> 898 - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> 899 - <Trans> 900 - Version {appVersion} {bundleInfo} 901 - </Trans> 902 - </Text> 903 - </TouchableOpacity> 904 - </View> 905 - 906 - <View 907 - style={[ 908 - {flexWrap: 'wrap', gap: 12, paddingHorizontal: 18}, 909 - s.flexRow, 910 - ]}> 911 - <TextLink 912 - type="md" 913 - style={pal.link} 914 - href="https://bsky.social/about/support/tos" 915 - text={_(msg`Terms of Service`)} 916 - /> 917 - <TextLink 918 - type="md" 919 - style={pal.link} 920 - href="https://bsky.social/about/support/privacy-policy" 921 - text={_(msg`Privacy Policy`)} 922 - /> 923 - <TextLink 924 - type="md" 925 - style={pal.link} 926 - href={STATUS_PAGE_URL} 927 - text={_(msg`Status Page`)} 928 - /> 929 - </View> 930 - <View style={s.footerSpacer} /> 931 - </ScrollView> 932 - </Layout.Screen> 933 - ) 934 - } 935 - 936 - function EmailConfirmationNotice() { 937 - const pal = usePalette('default') 938 - const palInverted = usePalette('inverted') 939 - const {_} = useLingui() 940 - const {isMobile} = useWebMediaQueries() 941 - const verifyEmailDialogControl = useDialogControl() 942 - 943 - return ( 944 - <View style={{marginBottom: 20}}> 945 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 946 - <Trans>Verify email</Trans> 947 - </Text> 948 - <View 949 - style={[ 950 - { 951 - paddingVertical: isMobile ? 12 : 0, 952 - paddingHorizontal: 18, 953 - }, 954 - pal.view, 955 - ]}> 956 - <View style={{flexDirection: 'row', marginBottom: 8}}> 957 - <Pressable 958 - style={[ 959 - palInverted.view, 960 - { 961 - flexDirection: 'row', 962 - gap: 6, 963 - borderRadius: 6, 964 - paddingHorizontal: 12, 965 - paddingVertical: 10, 966 - alignItems: 'center', 967 - }, 968 - isMobile && {flex: 1}, 969 - ]} 970 - accessibilityRole="button" 971 - accessibilityLabel={_(msg`Verify my email`)} 972 - accessibilityHint={_(msg`Opens modal for email verification`)} 973 - onPress={() => verifyEmailDialogControl.open()}> 974 - <FontAwesomeIcon 975 - icon="envelope" 976 - color={palInverted.colors.text} 977 - size={16} 978 - /> 979 - <Text type="button" style={palInverted.text}> 980 - <Trans>Verify My Email</Trans> 981 - </Text> 982 - </Pressable> 983 - </View> 984 - <Text style={pal.textLight}> 985 - <Trans>Protect your account by verifying your email.</Trans> 986 - </Text> 987 - </View> 988 - <VerifyEmailDialog control={verifyEmailDialogControl} /> 989 - </View> 990 - ) 991 - } 992 - 993 - const styles = StyleSheet.create({ 994 - dimmed: { 995 - opacity: 0.5, 996 - }, 997 - spacer20: { 998 - height: 20, 999 - }, 1000 - heading: { 1001 - paddingHorizontal: 18, 1002 - paddingBottom: 6, 1003 - }, 1004 - infoLine: { 1005 - flexDirection: 'row', 1006 - alignItems: 'center', 1007 - paddingHorizontal: 18, 1008 - paddingBottom: 6, 1009 - }, 1010 - profile: { 1011 - flexDirection: 'row', 1012 - marginVertical: 6, 1013 - borderRadius: 4, 1014 - paddingVertical: 10, 1015 - paddingHorizontal: 10, 1016 - }, 1017 - linkCard: { 1018 - flexDirection: 'row', 1019 - alignItems: 'center', 1020 - paddingVertical: 12, 1021 - paddingHorizontal: 18, 1022 - marginBottom: 1, 1023 - }, 1024 - linkCardNoIcon: { 1025 - flexDirection: 'row', 1026 - alignItems: 'center', 1027 - paddingVertical: 20, 1028 - paddingHorizontal: 18, 1029 - marginBottom: 1, 1030 - }, 1031 - toggleCard: { 1032 - paddingVertical: 8, 1033 - paddingHorizontal: 6, 1034 - marginBottom: 1, 1035 - }, 1036 - avi: { 1037 - marginRight: 12, 1038 - }, 1039 - iconContainer: { 1040 - alignItems: 'center', 1041 - justifyContent: 'center', 1042 - width: 40, 1043 - height: 40, 1044 - borderRadius: 30, 1045 - marginRight: 12, 1046 - }, 1047 - buildInfo: { 1048 - paddingVertical: 8, 1049 - }, 1050 - 1051 - colorModeText: { 1052 - marginLeft: 10, 1053 - marginBottom: 6, 1054 - }, 1055 - 1056 - selectableBtns: { 1057 - flexDirection: 'row', 1058 - }, 1059 - 1060 - btn: { 1061 - flexDirection: 'row', 1062 - alignItems: 'center', 1063 - justifyContent: 'center', 1064 - width: '100%', 1065 - borderRadius: 32, 1066 - padding: 14, 1067 - backgroundColor: colors.gray1, 1068 - }, 1069 - toggleBtn: { 1070 - paddingHorizontal: 0, 1071 - }, 1072 - footer: { 1073 - flex: 1, 1074 - flexDirection: 'row', 1075 - paddingLeft: 18, 1076 - }, 1077 - })