diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/modals/AddAppPasswords.tsx | 307 | ||||
-rw-r--r-- | src/view/com/modals/ChangeHandle.tsx | 614 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/AccountDropdownBtn.tsx | 66 | ||||
-rw-r--r-- | src/view/com/util/List.web.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/AccessibilitySettings.tsx | 157 | ||||
-rw-r--r-- | src/view/screens/AppPasswords.tsx | 375 | ||||
-rw-r--r-- | src/view/screens/LanguageSettings.tsx | 336 | ||||
-rw-r--r-- | src/view/screens/PreferencesExternalEmbeds.tsx | 147 | ||||
-rw-r--r-- | src/view/screens/PreferencesFollowingFeed.tsx | 249 | ||||
-rw-r--r-- | src/view/screens/PreferencesThreads.tsx | 198 | ||||
-rw-r--r-- | src/view/screens/Settings/DisableEmail2FADialog.tsx | 201 | ||||
-rw-r--r-- | src/view/screens/Settings/Email2FAToggle.tsx | 58 | ||||
-rw-r--r-- | src/view/screens/Settings/ExportCarDialog.tsx | 110 | ||||
-rw-r--r-- | src/view/screens/Settings/index.tsx | 1077 |
16 files changed, 3 insertions, 3910 deletions
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx deleted file mode 100644 index f7991f59b..000000000 --- a/src/view/com/modals/AddAppPasswords.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import React, {useState} from 'react' -import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native' -import {setStringAsync} from 'expo-clipboard' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {s} from '#/lib/styles' -import {logger} from '#/logger' -import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import { - useAppPasswordCreateMutation, - useAppPasswordsQuery, -} from '#/state/queries/app-passwords' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a} from '#/alf' -import * as Toggle from '#/components/forms/Toggle' - -export const snapPoints = ['90%'] - -const shadesOfBlue: string[] = [ - 'AliceBlue', - 'Aqua', - 'Aquamarine', - 'Azure', - 'BabyBlue', - 'Blue', - 'BlueViolet', - 'CadetBlue', - 'CornflowerBlue', - 'Cyan', - 'DarkBlue', - 'DarkCyan', - 'DarkSlateBlue', - 'DeepSkyBlue', - 'DodgerBlue', - 'ElectricBlue', - 'LightBlue', - 'LightCyan', - 'LightSkyBlue', - 'LightSteelBlue', - 'MediumAquaMarine', - 'MediumBlue', - 'MediumSlateBlue', - 'MidnightBlue', - 'Navy', - 'PowderBlue', - 'RoyalBlue', - 'SkyBlue', - 'SlateBlue', - 'SteelBlue', - 'Teal', - 'Turquoise', -] - -export function Component({}: {}) { - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const {data: passwords} = useAppPasswordsQuery() - const {mutateAsync: mutateAppPassword, isPending} = - useAppPasswordCreateMutation() - const [name, setName] = useState( - shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], - ) - const [appPassword, setAppPassword] = useState<string>() - const [wasCopied, setWasCopied] = useState(false) - const [privileged, setPrivileged] = useState(false) - - const onCopy = React.useCallback(() => { - if (appPassword) { - setStringAsync(appPassword) - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') - setWasCopied(true) - } - }, [appPassword, _]) - - const onDone = React.useCallback(() => { - closeModal() - }, [closeModal]) - - const createAppPassword = async () => { - // if name is all whitespace, we don't allow it - if (!name || !name.trim()) { - Toast.show( - _( - msg`Please enter a name for your app password. All spaces is not allowed.`, - ), - 'xmark', - ) - return - } - // if name is too short (under 4 chars), we don't allow it - if (name.length < 4) { - Toast.show( - _(msg`App Password names must be at least 4 characters long.`), - 'xmark', - ) - return - } - - if (passwords?.find(p => p.name === name)) { - Toast.show(_(msg`This name is already in use`), 'xmark') - return - } - - try { - const newPassword = await mutateAppPassword({name, privileged}) - if (newPassword) { - setAppPassword(newPassword.password) - } else { - Toast.show(_(msg`Failed to create app password.`), 'xmark') - // TODO: better error handling (?) - } - } catch (e) { - Toast.show(_(msg`Failed to create app password.`), 'xmark') - logger.error('Failed to create app password', {message: e}) - } - } - - const _onChangeText = (text: string) => { - // sanitize input - // we only all alphanumeric characters, spaces, dashes, and underscores - // if the user enters anything else, we ignore it and shake the input container - // also, it cannot start with a space - if (text.match(/^[a-zA-Z0-9-_ ]*$/)) { - setName(text) - } else { - Toast.show( - _( - msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`, - ), - 'xmark', - ) - } - } - - return ( - <View style={[styles.container, pal.view]} testID="addAppPasswordsModal"> - {!appPassword ? ( - <> - <View> - <Text type="lg" style={[pal.text]}> - <Trans> - Please enter a unique name for this App Password or use our - randomly generated one. - </Trans> - </Text> - <View style={[pal.btn, styles.textInputWrapper]}> - <TextInput - style={[styles.input, pal.text]} - onChangeText={_onChangeText} - value={name} - placeholder={_(msg`Enter a name for this App Password`)} - placeholderTextColor={pal.colors.textLight} - autoCorrect={false} - autoComplete="off" - autoCapitalize="none" - autoFocus={true} - maxLength={32} - selectTextOnFocus={true} - blurOnSubmit={true} - editable={!isPending} - returnKeyType="done" - onSubmitEditing={createAppPassword} - accessible={true} - accessibilityLabel={_(msg`Name`)} - accessibilityHint={_(msg`Input name for app password`)} - /> - </View> - </View> - <Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}> - <Trans> - Can only contain letters, numbers, spaces, dashes, and - underscores. Must be at least 4 characters long, but no more than - 32 characters long. - </Trans> - </Text> - <Toggle.Item - type="checkbox" - label={_(msg`Allow access to your direct messages`)} - value={privileged} - onChange={val => setPrivileged(val)} - name="privileged" - style={a.my_md}> - <Toggle.Checkbox /> - <Toggle.LabelText> - <Trans>Allow access to your direct messages</Trans> - </Toggle.LabelText> - </Toggle.Item> - </> - ) : ( - <> - <View> - <Text type="lg" style={[pal.text]}> - <Text type="lg-bold" style={[pal.text, s.mr5]}> - <Trans>Here is your app password.</Trans> - </Text> - <Trans> - Use this to sign into the other app along with your handle. - </Trans> - </Text> - <TouchableOpacity - style={[pal.border, styles.passwordContainer, pal.btn]} - onPress={onCopy} - accessibilityRole="button" - accessibilityLabel={_(msg`Copy`)} - accessibilityHint={_(msg`Copies app password`)}> - <Text type="2xl-bold" style={[pal.text]}> - {appPassword} - </Text> - {wasCopied ? ( - <Text style={[pal.textLight]}> - <Trans>Copied</Trans> - </Text> - ) : ( - <FontAwesomeIcon - icon={['far', 'clone']} - style={pal.text as FontAwesomeIconStyle} - size={18} - /> - )} - </TouchableOpacity> - </View> - <Text type="lg" style={[pal.textLight, s.mb10]}> - <Trans> - For security reasons, you won't be able to view this again. If you - lose this password, you'll need to generate a new one. - </Trans> - </Text> - </> - )} - <View style={styles.btnContainer}> - <Button - type="primary" - label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={!appPassword ? createAppPassword : onDone} - /> - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: isNative ? 50 : 0, - paddingHorizontal: 16, - }, - textInputWrapper: { - borderRadius: 8, - flexDirection: 'row', - alignItems: 'center', - marginTop: 16, - marginBottom: 8, - }, - input: { - flex: 1, - width: '100%', - paddingVertical: 10, - paddingHorizontal: 8, - fontSize: 17, - letterSpacing: 0.25, - fontWeight: '400', - borderRadius: 10, - }, - passwordContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingVertical: 8, - paddingHorizontal: 16, - alignItems: 'center', - borderRadius: 10, - marginTop: 16, - marginBottom: 12, - }, - btnContainer: { - flexDirection: 'row', - justifyContent: 'center', - marginTop: 12, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - paddingHorizontal: 60, - paddingVertical: 14, - }, - btnLabel: { - fontSize: 18, - }, - groupContent: { - borderTopWidth: 1, - flexDirection: 'row', - alignItems: 'center', - }, -}) diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx deleted file mode 100644 index 2181a94aa..000000000 --- a/src/view/com/modals/ChangeHandle.tsx +++ /dev/null @@ -1,614 +0,0 @@ -import React, {useState} from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {setStringAsync} from 'expo-clipboard' -import {ComAtprotoServerDescribeServer} from '@atproto/api' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {cleanError} from '#/lib/strings/errors' -import {createFullHandle, makeValidHandle} from '#/lib/strings/handles' -import {s} from '#/lib/styles' -import {useTheme} from '#/lib/ThemeContext' -import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' -import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle' -import {useServiceQuery} from '#/state/queries/service' -import {SessionAccount, useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {Button} from '../util/forms/Button' -import {SelectableBtn} from '../util/forms/SelectableBtn' -import {Text} from '../util/text/Text' -import * as Toast from '../util/Toast' -import {ScrollView, TextInput} from './util' - -export const snapPoints = ['100%'] - -export type Props = {onChanged: () => void} - -export function Component(props: Props) { - const {currentAccount} = useSession() - const agent = useAgent() - const { - isLoading, - data: serviceInfo, - error: serviceInfoError, - } = useServiceQuery(agent.service.toString()) - - return isLoading || !currentAccount ? ( - <View style={{padding: 18}}> - <ActivityIndicator /> - </View> - ) : serviceInfoError || !serviceInfo ? ( - <ErrorMessage message={cleanError(serviceInfoError)} /> - ) : ( - <Inner - {...props} - currentAccount={currentAccount} - serviceInfo={serviceInfo} - /> - ) -} - -export function Inner({ - currentAccount, - serviceInfo, - onChanged, -}: Props & { - currentAccount: SessionAccount - serviceInfo: ComAtprotoServerDescribeServer.OutputSchema -}) { - const {_} = useLingui() - const pal = usePalette('default') - const {closeModal} = useModalControls() - const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} = - useUpdateHandleMutation() - const agent = useAgent() - - const [error, setError] = useState<string>('') - - const [isCustom, setCustom] = React.useState<boolean>(false) - const [handle, setHandle] = React.useState<string>('') - const [canSave, setCanSave] = React.useState<boolean>(false) - - const userDomain = serviceInfo.availableUserDomains?.[0] - - // events - // = - const onPressCancel = React.useCallback(() => { - closeModal() - }, [closeModal]) - const onToggleCustom = React.useCallback(() => { - // toggle between a provided domain vs a custom one - setHandle('') - setCanSave(false) - setCustom(!isCustom) - }, [setCustom, isCustom]) - const onPressSave = React.useCallback(async () => { - if (!userDomain) { - logger.error(`ChangeHandle: userDomain is undefined`, { - service: serviceInfo, - }) - setError(`The service you've selected has no domains configured.`) - return - } - - try { - const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) - logger.debug(`Updating handle to ${newHandle}`) - await updateHandle({ - handle: newHandle, - }) - await agent.resumeSession(agent.session!) - closeModal() - onChanged() - } catch (err: any) { - setError(cleanError(err)) - logger.error('Failed to update handle', {handle, message: err}) - } finally { - } - }, [ - setError, - handle, - userDomain, - isCustom, - onChanged, - closeModal, - updateHandle, - serviceInfo, - agent, - ]) - - // rendering - // = - return ( - <View style={[s.flex1, pal.view]}> - <View style={[styles.title, pal.border]}> - <View style={styles.titleLeft}> - <TouchableOpacity - onPress={onPressCancel} - accessibilityRole="button" - accessibilityLabel={_(msg`Cancel change handle`)} - accessibilityHint={_(msg`Exits handle change process`)} - onAccessibilityEscape={onPressCancel}> - <Text type="lg" style={pal.textLight}> - <Trans>Cancel</Trans> - </Text> - </TouchableOpacity> - </View> - <Text - type="2xl-bold" - style={[styles.titleMiddle, pal.text]} - numberOfLines={1}> - <Trans>Change Handle</Trans> - </Text> - <View style={styles.titleRight}> - {isUpdateHandlePending ? ( - <ActivityIndicator /> - ) : canSave ? ( - <TouchableOpacity - onPress={onPressSave} - accessibilityRole="button" - accessibilityLabel={_(msg`Save handle change`)} - accessibilityHint={_(msg`Saves handle change to ${handle}`)}> - <Text type="2xl-medium" style={pal.link}> - <Trans>Save</Trans> - </Text> - </TouchableOpacity> - ) : undefined} - </View> - </View> - <ScrollView style={styles.inner}> - {error !== '' && ( - <View style={styles.errorContainer}> - <ErrorMessage message={error} /> - </View> - )} - - {isCustom ? ( - <CustomHandleForm - currentAccount={currentAccount} - handle={handle} - isProcessing={isUpdateHandlePending} - canSave={canSave} - onToggleCustom={onToggleCustom} - setHandle={setHandle} - setCanSave={setCanSave} - onPressSave={onPressSave} - /> - ) : ( - <ProvidedHandleForm - handle={handle} - userDomain={userDomain} - isProcessing={isUpdateHandlePending} - onToggleCustom={onToggleCustom} - setHandle={setHandle} - setCanSave={setCanSave} - /> - )} - </ScrollView> - </View> - ) -} - -/** - * The form for using a domain allocated by the PDS - */ -function ProvidedHandleForm({ - userDomain, - handle, - isProcessing, - setHandle, - onToggleCustom, - setCanSave, -}: { - userDomain: string - handle: string - isProcessing: boolean - setHandle: (v: string) => void - onToggleCustom: () => void - setCanSave: (v: boolean) => void -}) { - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - - // events - // = - const onChangeHandle = React.useCallback( - (v: string) => { - const newHandle = makeValidHandle(v) - setHandle(newHandle) - setCanSave(newHandle.length > 0) - }, - [setHandle, setCanSave], - ) - - // rendering - // = - return ( - <> - <View style={[pal.btn, styles.textInputWrapper]}> - <FontAwesomeIcon - icon="at" - style={[pal.textLight, styles.textInputIcon]} - /> - <TextInput - testID="setHandleInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`e.g. alice`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - keyboardAppearance={theme.colorScheme} - value={handle} - onChangeText={onChangeHandle} - editable={!isProcessing} - accessible={true} - accessibilityLabel={_(msg`Handle`)} - accessibilityHint={_(msg`Sets Bluesky username`)} - /> - </View> - <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}> - <Trans> - Your full handle will be{' '} - <Text type="md-bold" style={pal.textLight}> - @{createFullHandle(handle, userDomain)} - </Text> - </Trans> - </Text> - <TouchableOpacity - onPress={onToggleCustom} - accessibilityRole="button" - accessibilityLabel={_(msg`Hosting provider`)} - accessibilityHint={_(msg`Opens modal for using custom domain`)}> - <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}> - <Trans>I have my own domain</Trans> - </Text> - </TouchableOpacity> - </> - ) -} - -/** - * The form for using a custom domain - */ -function CustomHandleForm({ - currentAccount, - handle, - canSave, - isProcessing, - setHandle, - onToggleCustom, - onPressSave, - setCanSave, -}: { - currentAccount: SessionAccount - handle: string - canSave: boolean - isProcessing: boolean - setHandle: (v: string) => void - onToggleCustom: () => void - onPressSave: () => void - setCanSave: (v: boolean) => void -}) { - const pal = usePalette('default') - const palSecondary = usePalette('secondary') - const palError = usePalette('error') - const theme = useTheme() - const {_} = useLingui() - const [isVerifying, setIsVerifying] = React.useState(false) - const [error, setError] = React.useState<string>('') - const [isDNSForm, setDNSForm] = React.useState<boolean>(true) - const fetchDid = useFetchDid() - // events - // = - const onPressCopy = React.useCallback(() => { - setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did) - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') - }, [currentAccount, isDNSForm, _]) - const onChangeHandle = React.useCallback( - (v: string) => { - setHandle(v) - setCanSave(false) - }, - [setHandle, setCanSave], - ) - const onPressVerify = React.useCallback(async () => { - if (canSave) { - onPressSave() - } - try { - setIsVerifying(true) - setError('') - const did = await fetchDid(handle) - if (did === currentAccount.did) { - setCanSave(true) - } else { - setError(`Incorrect DID returned (got ${did})`) - } - } catch (err: any) { - setError(cleanError(err)) - logger.error('Failed to verify domain', {handle, error: err}) - } finally { - setIsVerifying(false) - } - }, [ - handle, - currentAccount, - setIsVerifying, - setCanSave, - setError, - canSave, - onPressSave, - fetchDid, - ]) - - // rendering - // = - return ( - <> - <Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain"> - <Trans>Enter the domain you want to use</Trans> - </Text> - <View style={[pal.btn, styles.textInputWrapper]}> - <FontAwesomeIcon - icon="at" - style={[pal.textLight, styles.textInputIcon]} - /> - <TextInput - testID="setHandleInput" - style={[pal.text, styles.textInput]} - placeholder={_(msg`e.g. alice.com`)} - placeholderTextColor={pal.colors.textLight} - autoCapitalize="none" - keyboardAppearance={theme.colorScheme} - value={handle} - onChangeText={onChangeHandle} - editable={!isProcessing} - accessibilityLabelledBy="customDomain" - accessibilityLabel={_(msg`Custom domain`)} - accessibilityHint={_(msg`Input your preferred hosting provider`)} - /> - </View> - <View style={styles.spacer} /> - - <View style={[styles.selectableBtns]}> - <SelectableBtn - selected={isDNSForm} - label={_(msg`DNS Panel`)} - left - onSelect={() => setDNSForm(true)} - accessibilityHint={_(msg`Use the DNS panel`)} - style={s.flex1} - /> - <SelectableBtn - selected={!isDNSForm} - label={_(msg`No DNS Panel`)} - right - onSelect={() => setDNSForm(false)} - accessibilityHint={_(msg`Use a file on your server`)} - style={s.flex1} - /> - </View> - <View style={styles.spacer} /> - {isDNSForm ? ( - <> - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> - <Trans>Add the following DNS record to your domain:</Trans> - </Text> - <View style={[styles.dnsTable, pal.btn]}> - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> - <Trans>Host:</Trans> - </Text> - <View style={[styles.dnsValue]}> - <Text type="mono" style={[styles.monoText, pal.text]}> - _atproto - </Text> - </View> - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> - <Trans>Type:</Trans> - </Text> - <View style={[styles.dnsValue]}> - <Text type="mono" style={[styles.monoText, pal.text]}> - TXT - </Text> - </View> - <Text type="md-medium" style={[styles.dnsLabel, pal.text]}> - <Trans>Value:</Trans> - </Text> - <View style={[styles.dnsValue]}> - <Text type="mono" style={[styles.monoText, pal.text]}> - did={currentAccount.did} - </Text> - </View> - </View> - <Text type="md" style={[pal.text, s.pt20, s.pl5]}> - <Trans>This should create a domain record at:</Trans> - </Text> - <Text type="mono" style={[styles.monoText, pal.text, s.pt5, s.pl5]}> - _atproto.{handle} - </Text> - </> - ) : ( - <> - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> - <Trans>Upload a text file to:</Trans> - </Text> - <View style={[styles.valueContainer, pal.btn]}> - <View style={[styles.dnsValue]}> - <Text type="mono" style={[styles.monoText, pal.text]}> - https://{handle}/.well-known/atproto-did - </Text> - </View> - </View> - <View style={styles.spacer} /> - <Text type="md" style={[pal.text, s.pb5, s.pl5]}> - <Trans>That contains the following:</Trans> - </Text> - <View style={[styles.valueContainer, pal.btn]}> - <View style={[styles.dnsValue]}> - <Text type="mono" style={[styles.monoText, pal.text]}> - {currentAccount.did} - </Text> - </View> - </View> - </> - )} - - <View style={styles.spacer} /> - <Button type="default" style={[s.p20, s.mb10]} onPress={onPressCopy}> - <Text type="xl" style={[pal.link, s.textCenter]}> - <Trans> - Copy {isDNSForm ? _(msg`Domain Value`) : _(msg`File Contents`)} - </Trans> - </Text> - </Button> - {canSave === true && ( - <View style={[styles.message, palSecondary.view]}> - <Text type="md-medium" style={palSecondary.text}> - <Trans>Domain verified!</Trans> - </Text> - </View> - )} - {error ? ( - <View style={[styles.message, palError.view]}> - <Text type="md-medium" style={palError.text}> - {error} - </Text> - </View> - ) : null} - <Button - type="primary" - style={[s.p20, isVerifying && styles.dimmed]} - onPress={onPressVerify}> - {isVerifying ? ( - <ActivityIndicator color="white" /> - ) : ( - <Text type="xl-medium" style={[s.white, s.textCenter]}> - {canSave - ? _(msg`Update to ${handle}`) - : isDNSForm - ? _(msg`Verify DNS Record`) - : _(msg`Verify Text File`)} - </Text> - )} - </Button> - <View style={styles.spacer} /> - <TouchableOpacity - onPress={onToggleCustom} - accessibilityLabel={_(msg`Use default provider`)} - accessibilityHint={_(msg`Use bsky.social as hosting provider`)}> - <Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}> - <Trans>Nevermind, create a handle for me</Trans> - </Text> - </TouchableOpacity> - </> - ) -} - -const styles = StyleSheet.create({ - inner: { - padding: 14, - }, - footer: { - padding: 14, - }, - spacer: { - height: 20, - }, - dimmed: { - opacity: 0.7, - }, - - selectableBtns: { - flexDirection: 'row', - }, - - title: { - flexDirection: 'row', - alignItems: 'center', - paddingTop: 25, - paddingHorizontal: 20, - paddingBottom: 15, - borderBottomWidth: 1, - }, - titleLeft: { - width: 80, - }, - titleRight: { - width: 80, - flexDirection: 'row', - justifyContent: 'flex-end', - }, - titleMiddle: { - flex: 1, - textAlign: 'center', - fontSize: 21, - }, - - textInputWrapper: { - borderRadius: 8, - flexDirection: 'row', - alignItems: 'center', - }, - textInputIcon: { - marginLeft: 12, - }, - textInput: { - flex: 1, - width: '100%', - paddingVertical: 10, - paddingHorizontal: 8, - fontSize: 17, - letterSpacing: 0.25, - fontWeight: '400', - borderRadius: 10, - }, - - valueContainer: { - borderRadius: 4, - paddingVertical: 16, - }, - - dnsTable: { - borderRadius: 4, - paddingTop: 2, - paddingBottom: 16, - }, - dnsLabel: { - paddingHorizontal: 14, - paddingTop: 10, - }, - dnsValue: { - paddingHorizontal: 14, - borderRadius: 4, - }, - monoText: { - fontSize: 18, - lineHeight: 20, - }, - - message: { - paddingHorizontal: 12, - paddingVertical: 10, - borderRadius: 8, - marginBottom: 10, - }, - - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 10, - marginBottom: 10, - }, - errorContainer: {marginBottom: 10}, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index becb39ff3..78f4a0117 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -7,9 +7,7 @@ import {usePalette} from '#/lib/hooks/usePalette' import {useModalControls, useModals} from '#/state/modals' import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as AddAppPassword from './AddAppPasswords' import * as ChangeEmailModal from './ChangeEmail' -import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as DeleteAccountModal from './DeleteAccount' @@ -69,15 +67,9 @@ export function ModalsContainer() { } else if (activeModal?.name === 'delete-account') { snapPoints = DeleteAccountModal.snapPoints element = <DeleteAccountModal.Component /> - } else if (activeModal?.name === 'change-handle') { - snapPoints = ChangeHandleModal.snapPoints - element = <ChangeHandleModal.Component {...activeModal} /> } else if (activeModal?.name === 'invite-codes') { snapPoints = InviteCodesModal.snapPoints element = <InviteCodesModal.Component /> - } else if (activeModal?.name === 'add-app-password') { - snapPoints = AddAppPassword.snapPoints - element = <AddAppPassword.Component /> } else if (activeModal?.name === 'content-languages-settings') { snapPoints = ContentLanguagesSettingsModal.snapPoints element = <ContentLanguagesSettingsModal.Component /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 46ced58d9..e9d9c01dd 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -7,9 +7,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import type {Modal as ModalIface} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals' -import * as AddAppPassword from './AddAppPasswords' import * as ChangeEmailModal from './ChangeEmail' -import * as ChangeHandleModal from './ChangeHandle' import * as ChangePasswordModal from './ChangePassword' import * as CreateOrEditListModal from './CreateOrEditList' import * as CropImageModal from './CropImage.web' @@ -74,12 +72,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <CropImageModal.Component {...modal} /> } else if (modal.name === 'delete-account') { element = <DeleteAccountModal.Component /> - } else if (modal.name === 'change-handle') { - element = <ChangeHandleModal.Component {...modal} /> } else if (modal.name === 'invite-codes') { element = <InviteCodesModal.Component /> - } else if (modal.name === 'add-app-password') { - element = <AddAppPassword.Component /> } else if (modal.name === 'content-languages-settings') { element = <ContentLanguagesSettingsModal.Component /> } else if (modal.name === 'post-languages-settings') { diff --git a/src/view/com/util/AccountDropdownBtn.tsx b/src/view/com/util/AccountDropdownBtn.tsx deleted file mode 100644 index e7985bccf..000000000 --- a/src/view/com/util/AccountDropdownBtn.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react' -import {Pressable} from 'react-native' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {usePalette} from '#/lib/hooks/usePalette' -import {s} from '#/lib/styles' -import {SessionAccount, useSessionApi} from '#/state/session' -import {useDialogControl} from '#/components/Dialog' -import * as Prompt from '#/components/Prompt' -import * as Toast from '../../com/util/Toast' -import {DropdownItem, NativeDropdown} from './forms/NativeDropdown' - -export function AccountDropdownBtn({account}: {account: SessionAccount}) { - const pal = usePalette('default') - const {removeAccount} = useSessionApi() - const removePromptControl = useDialogControl() - const {_} = useLingui() - - const items: DropdownItem[] = [ - { - label: _(msg`Remove account`), - onPress: removePromptControl.open, - icon: { - ios: { - name: 'trash', - }, - android: 'ic_delete', - web: ['far', 'trash-can'], - }, - }, - ] - return ( - <> - <Pressable accessibilityRole="button" style={s.pl10}> - <NativeDropdown - testID="accountSettingsDropdownBtn" - items={items} - accessibilityLabel={_(msg`Account options`)} - accessibilityHint=""> - <FontAwesomeIcon - icon="ellipsis-h" - style={pal.textLight as FontAwesomeIconStyle} - /> - </NativeDropdown> - </Pressable> - <Prompt.Basic - control={removePromptControl} - title={_(msg`Remove from quick access?`)} - description={_( - msg`This will remove @${account.handle} from the quick access list.`, - )} - onConfirm={() => { - removeAccount(account) - Toast.show(_(msg`Account removed from quick access`)) - }} - confirmButtonCta={_(msg`Remove`)} - confirmButtonColor="negative" - /> - </> - ) -} diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 59a79b531..5ddc4ea8a 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -448,7 +448,9 @@ let Row = function RowImpl<ItemT>({ onItemSeen: ((item: any) => void) | undefined }): React.ReactNode { const rowRef = React.useRef(null) - const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined) + const intersectionTimeout = React.useRef< + ReturnType<typeof setTimeout> | undefined + >(undefined) const handleIntersection = useNonReactiveCallback( (entries: IntersectionObserverEntry[]) => { diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx deleted file mode 100644 index 4dd5aa97b..000000000 --- a/src/view/screens/AccessibilitySettings.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {isNative} from '#/platform/detection' -import { - useAutoplayDisabled, - useHapticsDisabled, - useRequireAltTextEnabled, - useSetAutoplayDisabled, - useSetHapticsDisabled, - useSetRequireAltTextEnabled, -} from '#/state/preferences' -import { - useLargeAltBadgeEnabled, - useSetLargeAltBadgeEnabled, -} from '#/state/preferences/large-alt-badge' -import {useSetMinimalShellMode} from '#/state/shell' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'AccessibilitySettings' -> -export function AccessibilitySettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewAccessibilitySettingsScreen {...props} /> - ) : ( - <LegacyAccessibilitySettingsScreen {...props} /> - ) -} - -function LegacyAccessibilitySettingsScreen({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile, isTabletOrMobile} = useWebMediaQueries() - const {_} = useLingui() - - const requireAltTextEnabled = useRequireAltTextEnabled() - const setRequireAltTextEnabled = useSetRequireAltTextEnabled() - const autoplayDisabled = useAutoplayDisabled() - const setAutoplayDisabled = useSetAutoplayDisabled() - const hapticsDisabled = useHapticsDisabled() - const setHapticsDisabled = useSetHapticsDisabled() - const largeAltBadgeEnabled = useLargeAltBadgeEnabled() - const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - return ( - <Layout.Screen testID="accessibilitySettingsScreen"> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[ - pal.border, - a.border_b, - !isMobile && { - borderLeftWidth: StyleSheet.hairlineWidth, - borderRightWidth: StyleSheet.hairlineWidth, - }, - ]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Accessibility Settings</Trans> - </Text> - </View> - </SimpleViewHeader> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - style={s.flex1} - contentContainerStyle={[ - s.flex1, - {paddingBottom: 100}, - isMobile && pal.viewLight, - ]}> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Alt text</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Require alt text before posting`)} - labelType="lg" - isSelected={requireAltTextEnabled} - onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)} - /> - <ToggleButton - type="default-light" - label={_(msg`Display larger alt text badges`)} - labelType="lg" - isSelected={!!largeAltBadgeEnabled} - onPress={() => setLargeAltBadgeEnabled(!largeAltBadgeEnabled)} - /> - </View> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Media</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Disable autoplay for videos and GIFs`)} - labelType="lg" - isSelected={autoplayDisabled} - onPress={() => setAutoplayDisabled(!autoplayDisabled)} - /> - </View> - {isNative && ( - <> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Haptics</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Disable haptic feedback`)} - labelType="lg" - isSelected={hapticsDisabled} - onPress={() => setHapticsDisabled(!hapticsDisabled)} - /> - </View> - </> - )} - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - heading: { - paddingHorizontal: 18, - paddingTop: 14, - paddingBottom: 6, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, -}) diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx deleted file mode 100644 index 09da3c1d2..000000000 --- a/src/view/screens/AppPasswords.tsx +++ /dev/null @@ -1,375 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {ScrollView} from 'react-native-gesture-handler' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' -import {NativeStackScreenProps} from '@react-navigation/native-stack' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams} from '#/lib/routes/types' -import {cleanError} from '#/lib/strings/errors' -import {useModalControls} from '#/state/modals' -import { - useAppPasswordDeleteMutation, - useAppPasswordsQuery, -} from '#/state/queries/app-passwords' -import {useSetMinimalShellMode} from '#/state/shell' -import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords' -import {atoms as a} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import * as Layout from '#/components/Layout' -import * as Prompt from '#/components/Prompt' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> -export function AppPasswords(props: Props) { - return IS_INTERNAL ? ( - <NewAppPasswordsScreen {...props} /> - ) : ( - <Layout.Screen testID="AppPasswordsScreen"> - <AppPasswordsInner /> - </Layout.Screen> - ) -} - -function AppPasswordsInner() { - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrDesktop} = useWebMediaQueries() - const {openModal} = useModalControls() - const {data: appPasswords, error} = useAppPasswordsQuery() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onAdd = React.useCallback(async () => { - openModal({name: 'add-app-password'}) - }, [openModal]) - - if (error) { - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <ErrorScreen - title={_(msg`Oops!`)} - message={_(msg`There was an issue with fetching your app passwords`)} - details={cleanError(error)} - /> - </CenteredView> - ) - } - - // no app passwords (empty) state - if (appPasswords?.length === 0) { - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <AppPasswordsHeader /> - <View style={[styles.empty, pal.viewLight]}> - <Text type="lg" style={[pal.text, styles.emptyText]}> - <Trans> - You have not created any app passwords yet. You can create one by - pressing the button below. - </Trans> - </Text> - </View> - {!isTabletOrDesktop && <View style={styles.flex1} />} - <View - style={[ - styles.btnContainer, - isTabletOrDesktop && styles.btnContainerDesktop, - ]}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - </CenteredView> - ) - } - - if (appPasswords?.length) { - // has app passwords - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <AppPasswordsHeader /> - <ScrollView - style={[ - styles.scrollContainer, - pal.border, - !isTabletOrDesktop && styles.flex1, - ]}> - {appPasswords.map((password, i) => ( - <AppPassword - key={password.name} - testID={`appPassword-${i}`} - name={password.name} - createdAt={password.createdAt} - privileged={password.privileged} - /> - ))} - {isTabletOrDesktop && ( - <View style={[styles.btnContainer, styles.btnContainerDesktop]}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - )} - </ScrollView> - {!isTabletOrDesktop && ( - <View style={styles.btnContainer}> - <Button - testID="appPasswordBtn" - type="primary" - label={_(msg`Add App Password`)} - style={styles.btn} - labelStyle={styles.btnLabel} - onPress={onAdd} - /> - </View> - )} - </CenteredView> - ) - } - - return ( - <CenteredView - style={[ - styles.container, - isTabletOrDesktop && styles.containerDesktop, - pal.view, - pal.border, - ]} - testID="appPasswordsScreen"> - <ActivityIndicator /> - </CenteredView> - ) -} - -function AppPasswordsHeader() { - const {isTabletOrDesktop} = useWebMediaQueries() - const pal = usePalette('default') - const {_} = useLingui() - return ( - <> - <ViewHeader title={_(msg`App Passwords`)} showOnDesktop /> - <Text - type="sm" - style={[ - styles.description, - pal.text, - isTabletOrDesktop && styles.descriptionDesktop, - ]}> - <Trans> - Use app passwords to login to other Bluesky clients without giving - full access to your account or password. - </Trans> - </Text> - </> - ) -} - -function AppPassword({ - testID, - name, - createdAt, - privileged, -}: { - testID: string - name: string - createdAt: string - privileged?: boolean -}) { - const pal = usePalette('default') - const {_, i18n} = useLingui() - const control = useDialogControl() - const deleteMutation = useAppPasswordDeleteMutation() - - const onDelete = React.useCallback(async () => { - await deleteMutation.mutateAsync({name}) - Toast.show(_(msg`App password deleted`)) - }, [deleteMutation, name, _]) - - const onPress = React.useCallback(() => { - control.open() - }, [control]) - - return ( - <TouchableOpacity - testID={testID} - style={[styles.item, pal.border]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete app password`)} - accessibilityHint=""> - <View> - <Text type="md-bold" style={pal.text}> - {name} - </Text> - <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}> - <Trans> - Created{' '} - {i18n.date(createdAt, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - </Trans> - </Text> - {privileged && ( - <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_xs]}> - <FontAwesomeIcon - icon="circle-exclamation" - color={pal.colors.textLight} - size={14} - /> - <Text type="md" style={pal.textLight}> - <Trans>Allows access to direct messages</Trans> - </Text> - </View> - )} - </View> - <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> - - <Prompt.Basic - control={control} - title={_(msg`Delete app password?`)} - description={_( - msg`Are you sure you want to delete the app password "${name}"?`, - )} - onConfirm={onDelete} - confirmButtonCta={_(msg`Delete`)} - confirmButtonColor="negative" - /> - </TouchableOpacity> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 100, - }, - containerDesktop: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 0, - }, - title: { - textAlign: 'center', - marginTop: 12, - marginBottom: 12, - }, - description: { - textAlign: 'center', - paddingHorizontal: 20, - marginBottom: 14, - }, - descriptionDesktop: { - marginTop: 14, - }, - - scrollContainer: { - borderTopWidth: 1, - marginTop: 4, - marginBottom: 16, - }, - - flex1: { - flex: 1, - }, - empty: { - paddingHorizontal: 20, - paddingVertical: 20, - borderRadius: 16, - marginHorizontal: 24, - marginTop: 10, - }, - emptyText: { - textAlign: 'center', - }, - - item: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomWidth: 1, - paddingHorizontal: 20, - paddingVertical: 14, - }, - pr10: { - marginRight: 10, - }, - btnContainer: { - flexDirection: 'row', - justifyContent: 'center', - }, - btnContainerDesktop: { - marginTop: 14, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - paddingHorizontal: 60, - paddingVertical: 14, - }, - btnLabel: { - fontSize: 18, - }, - - trashIcon: { - color: 'red', - minWidth: 16, - }, -}) diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx deleted file mode 100644 index f99cccee9..000000000 --- a/src/view/screens/LanguageSettings.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect} from '@react-navigation/native' - -import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages' -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {s} from '#/lib/styles' -import {sanitizeAppLanguageSetting} from '#/locale/helpers' -import {useModalControls} from '#/state/modals' -import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' -import {useSetMinimalShellMode} from '#/state/shell' -import {Button} from '#/view/com/util/forms/Button' -import {Text} from '#/view/com/util/text/Text' -import {ViewHeader} from '#/view/com/util/ViewHeader' -import {CenteredView} from '#/view/com/util/Views' -import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> - -export function LanguageSettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewLanguageSettingsScreen {...props} /> - ) : ( - <LegacyLanguageSettingsScreen {...props} /> - ) -} - -function LegacyLanguageSettingsScreen(_props: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const langPrefs = useLanguagePrefs() - const setLangPrefs = useLanguagePrefsApi() - const {isTabletOrDesktop} = useWebMediaQueries() - const setMinimalShellMode = useSetMinimalShellMode() - const {openModal} = useModalControls() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onPressContentLanguages = React.useCallback(() => { - openModal({name: 'content-languages-settings'}) - }, [openModal]) - - const onChangePrimaryLanguage = React.useCallback( - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { - if (!value) return - if (langPrefs.primaryLanguage !== value) { - setLangPrefs.setPrimaryLanguage(value) - } - }, - [langPrefs, setLangPrefs], - ) - - const onChangeAppLanguage = React.useCallback( - (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { - if (!value) return - if (langPrefs.appLanguage !== value) { - setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) - } - }, - [langPrefs, setLangPrefs], - ) - - const myLanguages = React.useMemo(() => { - return ( - langPrefs.contentLanguages - .map(lang => LANGUAGES.find(l => l.code2 === lang)) - .filter(Boolean) - // @ts-ignore - .map(l => l.name) - .join(', ') - ) - }, [langPrefs.contentLanguages]) - - return ( - <Layout.Screen testID="PreferencesLanguagesScreen"> - <CenteredView - style={[ - pal.view, - pal.border, - styles.container, - isTabletOrDesktop && styles.desktopContainer, - ]}> - <ViewHeader title={_(msg`Language Settings`)} showOnDesktop /> - - <View style={{paddingTop: 20, paddingHorizontal: 20}}> - {/* APP LANGUAGE */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>App Language</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select your app language for the default text to display in the - app. - </Trans> - </Text> - - <View style={{position: 'relative'}}> - <RNPickerSelect - placeholder={{}} - value={sanitizeAppLanguageSetting(langPrefs.appLanguage)} - onValueChange={onChangeAppLanguage} - items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputIOS: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - - inputWeb: { - cursor: 'pointer', - // @ts-ignore web only - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 0, - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - fontFamily: 'inherit', - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - }} - /> - - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - backgroundColor: pal.viewLight.backgroundColor, - borderRadius: 24, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View - style={{ - height: 1, - backgroundColor: pal.border.borderColor, - marginBottom: 20, - }} - /> - - {/* PRIMARY LANGUAGE */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Primary Language</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select your preferred language for translations in your feed. - </Trans> - </Text> - - <View style={{position: 'relative'}}> - <RNPickerSelect - placeholder={{}} - value={langPrefs.primaryLanguage} - onValueChange={onChangePrimaryLanguage} - items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ - label: l.name, - value: l.code2, - key: l.code2 + l.code3, - }))} - style={{ - inputAndroid: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputIOS: { - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - inputWeb: { - cursor: 'pointer', - // @ts-ignore web only - '-moz-appearance': 'none', - '-webkit-appearance': 'none', - appearance: 'none', - outline: 0, - borderWidth: 0, - backgroundColor: pal.viewLight.backgroundColor, - color: pal.text.color, - fontSize: 14, - fontFamily: 'inherit', - letterSpacing: 0.5, - fontWeight: '600', - paddingHorizontal: 14, - paddingVertical: 8, - borderRadius: 24, - }, - }} - /> - - <View - style={{ - position: 'absolute', - top: 1, - right: 1, - bottom: 1, - width: 40, - backgroundColor: pal.viewLight.backgroundColor, - borderRadius: 24, - pointerEvents: 'none', - alignItems: 'center', - justifyContent: 'center', - }}> - <FontAwesomeIcon - icon="chevron-down" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - </View> - </View> - - <View - style={{ - height: 1, - backgroundColor: pal.border.borderColor, - marginBottom: 20, - }} - /> - - {/* CONTENT LANGUAGES */} - <View style={{paddingBottom: 20}}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Content Languages</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Select which languages you want your subscribed feeds to - include. If none are selected, all languages will be shown. - </Trans> - </Text> - - <Button - type="default" - onPress={onPressContentLanguages} - style={styles.button}> - <FontAwesomeIcon - icon={myLanguages.length ? 'check' : 'plus'} - style={pal.text as FontAwesomeIconStyle} - /> - <Text - type="button" - style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]} - numberOfLines={1}> - {myLanguages.length ? myLanguages : _(msg`Select languages`)} - </Text> - </Button> - </View> - </View> - </CenteredView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: 90, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - paddingBottom: 40, - }, - button: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, -}) diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx deleted file mode 100644 index ef3f73b3c..000000000 --- a/src/view/screens/PreferencesExternalEmbeds.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {Trans} from '@lingui/macro' -import {useFocusEffect} from '@react-navigation/native' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import { - EmbedPlayerSource, - externalEmbedLabels, -} from '#/lib/strings/embed-player' -import { - useExternalEmbedsPrefs, - useSetExternalEmbedPref, -} from '#/state/preferences' -import {useSetMinimalShellMode} from '#/state/shell' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'PreferencesExternalEmbeds' -> -export function PreferencesExternalEmbeds(props: Props) { - return IS_INTERNAL ? ( - <ExternalMediaPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesExternalEmbeds {...props} /> - ) -} - -function LegacyPreferencesExternalEmbeds({}: Props) { - const pal = usePalette('default') - const setMinimalShellMode = useSetMinimalShellMode() - const {isTabletOrMobile} = useWebMediaQueries() - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - return ( - <Layout.Screen testID="preferencesExternalEmbedsScreen"> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>External Media Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans>Customize media from external sites.</Trans> - </Text> - </View> - </SimpleViewHeader> - - <View style={[pal.view]}> - <View style={styles.infoCard}> - <Text style={pal.text}> - <Trans> - External media may allow websites to collect information about - you and your device. No information is sent or requested until - you press the "play" button. - </Trans> - </Text> - </View> - </View> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Enable media players for</Trans> - </Text> - {Object.entries(externalEmbedLabels) - // TODO: Remove special case when we disable the old integration. - .filter(([key]) => key !== 'tenor') - .map(([key, label]) => ( - <PrefSelector - source={key as EmbedPlayerSource} - label={label} - key={key} - /> - ))} - </ScrollView> - </Layout.Screen> - ) -} - -function PrefSelector({ - source, - label, -}: { - source: EmbedPlayerSource - label: string -}) { - const pal = usePalette('default') - const setExternalEmbedPref = useSetExternalEmbedPref() - const sources = useExternalEmbedsPrefs() - - return ( - <View> - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={label} - labelType="lg" - isSelected={sources?.[source] === 'show'} - onPress={() => - setExternalEmbedPref( - source, - sources?.[source] === 'show' ? 'hide' : 'show', - ) - } - /> - </View> - </View> - ) -} - -const styles = StyleSheet.create({ - heading: { - paddingHorizontal: 18, - paddingTop: 14, - paddingBottom: 14, - }, - spacer: { - height: 8, - }, - infoCard: { - paddingHorizontal: 20, - paddingVertical: 14, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, -}) diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx deleted file mode 100644 index c31a23c49..000000000 --- a/src/view/screens/PreferencesFollowingFeed.tsx +++ /dev/null @@ -1,249 +0,0 @@ -import React from 'react' -import {StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import { - usePreferencesQuery, - useSetFeedViewPreferencesMutation, -} from '#/state/queries/preferences' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps< - CommonNavigatorParams, - 'PreferencesFollowingFeed' -> -export function PreferencesFollowingFeed(props: Props) { - return IS_INTERNAL ? ( - <FollowingFeedPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesFollowingFeed {...props} /> - ) -} - -function LegacyPreferencesFollowingFeed({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() - const {data: preferences} = usePreferencesQuery() - const {mutate: setFeedViewPref, variables} = - useSetFeedViewPreferencesMutation() - - const showReplies = !( - variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies - ) - - return ( - <Layout.Screen testID="preferencesHomeFeedScreen"> - <ScrollView - // @ts-ignore web only -sfn - dataSet={{'stable-gutters': 1}} - contentContainerStyle={{paddingBottom: 75}}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Following Feed Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans> - Fine-tune the content you see on your Following feed. - </Trans> - </Text> - </View> - </SimpleViewHeader> - <View style={styles.cardsContainer}> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Replies</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all replies from your feed. - </Trans> - </Text> - <ToggleButton - testID="toggleRepliesBtn" - type="default-light" - label={showReplies ? _(msg`Yes`) : _(msg`No`)} - isSelected={showReplies} - onPress={() => - setFeedViewPref({ - hideReplies: !( - variables?.hideReplies ?? - preferences?.feedViewPrefs?.hideReplies - ), - }) - } - /> - </View> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Reposts</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all reposts from your feed. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ? _(msg`No`) - : _(msg`Yes`) - } - isSelected={ - !( - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ) - } - onPress={() => - setFeedViewPref({ - hideReposts: !( - variables?.hideReposts ?? - preferences?.feedViewPrefs?.hideReposts - ), - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Show Quote Posts</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "No" to hide all quote posts from your feed. - Reposts will still be visible. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ? _(msg`No`) - : _(msg`Yes`) - } - isSelected={ - !( - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ) - } - onPress={() => - setFeedViewPref({ - hideQuotePosts: !( - variables?.hideQuotePosts ?? - preferences?.feedViewPrefs?.hideQuotePosts - ), - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} - <Trans>Show Posts from My Feeds</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "Yes" to show samples of your saved feeds in - your Following feed. This is an experimental feature. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={ - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ? _(msg`Yes`) - : _(msg`No`) - } - isSelected={ - !!( - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ) - } - onPress={() => - setFeedViewPref({ - lab_mergeFeedEnabled: !( - variables?.lab_mergeFeedEnabled ?? - preferences?.feedViewPrefs?.lab_mergeFeedEnabled - ), - }) - } - /> - </View> - </View> - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - titleSection: { - paddingBottom: 30, - }, - title: { - textAlign: 'center', - marginBottom: 5, - }, - description: { - textAlign: 'center', - paddingHorizontal: 32, - }, - cardsContainer: { - paddingHorizontal: 20, - paddingVertical: 16, - }, - card: { - padding: 16, - borderRadius: 10, - marginBottom: 20, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnDesktop: { - marginHorizontal: 'auto', - paddingHorizontal: 80, - }, - btnContainer: { - paddingTop: 20, - }, - dimmed: { - opacity: 0.3, - }, -}) diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx deleted file mode 100644 index f511f4c59..000000000 --- a/src/view/screens/PreferencesThreads.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React from 'react' -import {ActivityIndicator, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {IS_INTERNAL} from '#/lib/app-info' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import { - usePreferencesQuery, - useSetThreadViewPreferencesMutation, -} from '#/state/queries/preferences' -import {RadioGroup} from '#/view/com/util/forms/RadioGroup' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import {ScrollView} from '#/view/com/util/Views' -import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' -import {atoms as a} from '#/alf' -import * as Layout from '#/components/Layout' - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'> -export function PreferencesThreads(props: Props) { - return IS_INTERNAL ? ( - <ThreadPreferencesScreen {...props} /> - ) : ( - <LegacyPreferencesThreads {...props} /> - ) -} - -function LegacyPreferencesThreads({}: Props) { - const pal = usePalette('default') - const {_} = useLingui() - const {isTabletOrMobile} = useWebMediaQueries() - const {data: preferences} = usePreferencesQuery() - const {mutate: setThreadViewPrefs, variables} = - useSetThreadViewPreferencesMutation() - - const prioritizeFollowedUsers = Boolean( - variables?.prioritizeFollowedUsers ?? - preferences?.threadViewPrefs?.prioritizeFollowedUsers, - ) - const treeViewEnabled = Boolean( - variables?.lab_treeViewEnabled ?? - preferences?.threadViewPrefs?.lab_treeViewEnabled, - ) - - return ( - <Layout.Screen testID="preferencesThreadsScreen"> - <ScrollView - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}} - contentContainerStyle={{paddingBottom: 75}}> - <SimpleViewHeader - showBackButton={isTabletOrMobile} - style={[pal.border, a.border_b]}> - <View style={a.flex_1}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Thread Preferences</Trans> - </Text> - <Text style={pal.textLight}> - <Trans>Fine-tune the discussion threads.</Trans> - </Text> - </View> - </SimpleViewHeader> - - {preferences ? ( - <View style={styles.cardsContainer}> - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Sort Replies</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans>Sort replies to the same post by:</Trans> - </Text> - <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}> - <RadioGroup - type="default-light" - items={[ - {key: 'oldest', label: _(msg`Oldest replies first`)}, - {key: 'newest', label: _(msg`Newest replies first`)}, - { - key: 'most-likes', - label: _(msg`Most-liked replies first`), - }, - { - key: 'random', - label: _(msg`Random (aka "Poster's Roulette")`), - }, - ]} - onSelect={key => setThreadViewPrefs({sort: key})} - initialSelection={preferences?.threadViewPrefs?.sort} - /> - </View> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <Trans>Prioritize Your Follows</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Show replies by people you follow before all other replies. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)} - isSelected={prioritizeFollowedUsers} - onPress={() => - setThreadViewPrefs({ - prioritizeFollowedUsers: !prioritizeFollowedUsers, - }) - } - /> - </View> - - <View style={[pal.viewLight, styles.card]}> - <Text type="title-sm" style={[pal.text, s.pb5]}> - <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '} - <Trans>Threaded Mode</Trans> - </Text> - <Text style={[pal.text, s.pb10]}> - <Trans> - Set this setting to "Yes" to show replies in a threaded view. - This is an experimental feature. - </Trans> - </Text> - <ToggleButton - type="default-light" - label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)} - isSelected={treeViewEnabled} - onPress={() => - setThreadViewPrefs({ - lab_treeViewEnabled: !treeViewEnabled, - }) - } - /> - </View> - </View> - ) : ( - <ActivityIndicator style={a.flex_1} /> - )} - </ScrollView> - </Layout.Screen> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - desktopContainer: { - borderLeftWidth: 1, - borderRightWidth: 1, - }, - titleSection: { - paddingBottom: 30, - }, - title: { - textAlign: 'center', - marginBottom: 5, - }, - description: { - textAlign: 'center', - paddingHorizontal: 32, - }, - cardsContainer: { - paddingHorizontal: 20, - paddingVertical: 16, - }, - card: { - padding: 16, - borderRadius: 10, - marginBottom: 20, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnDesktop: { - marginHorizontal: 'auto', - paddingHorizontal: 80, - }, - btnContainer: { - paddingTop: 20, - }, - dimmed: { - opacity: 0.3, - }, -}) diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/view/screens/Settings/DisableEmail2FADialog.tsx deleted file mode 100644 index 1378759b0..000000000 --- a/src/view/screens/Settings/DisableEmail2FADialog.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React, {useState} from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {cleanError} from '#/lib/strings/errors' -import {isNative} from '#/platform/detection' -import {useAgent, useSession} from '#/state/session' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as TextField from '#/components/forms/TextField' -import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' -import {Loader} from '#/components/Loader' -import {P, Text} from '#/components/Typography' - -enum Stages { - Email, - ConfirmCode, -} - -export function DisableEmail2FADialog({ - control, -}: { - control: Dialog.DialogOuterProps['control'] -}) { - const {_} = useLingui() - const t = useTheme() - const {gtMobile} = useBreakpoints() - const {currentAccount} = useSession() - const agent = useAgent() - - const [stage, setStage] = useState<Stages>(Stages.Email) - const [confirmationCode, setConfirmationCode] = useState<string>('') - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [error, setError] = useState<string>('') - - const onSendEmail = async () => { - setError('') - setIsProcessing(true) - try { - await agent.com.atproto.server.requestEmailUpdate() - setStage(Stages.ConfirmCode) - } catch (e) { - setError(cleanError(String(e))) - } finally { - setIsProcessing(false) - } - } - - const onConfirmDisable = async () => { - setError('') - setIsProcessing(true) - try { - if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ - email: currentAccount!.email, - token: confirmationCode.trim(), - emailAuthFactor: false, - }) - await agent.resumeSession(agent.session!) - Toast.show(_(msg`Email 2FA disabled`)) - } - control.close() - } catch (e) { - const errMsg = String(e) - if (errMsg.includes('Token is invalid')) { - setError(_(msg`Invalid 2FA confirmation code.`)) - } else { - setError(cleanError(errMsg)) - } - } finally { - setIsProcessing(false) - } - } - - return ( - <Dialog.Outer control={control}> - <Dialog.Handle /> - <Dialog.ScrollableInner - accessibilityDescribedBy="dialog-description" - accessibilityLabelledBy="dialog-title"> - <View style={[a.relative, a.gap_md, a.w_full]}> - <Text - nativeID="dialog-title" - style={[a.text_2xl, a.font_bold, t.atoms.text]}> - <Trans>Disable Email 2FA</Trans> - </Text> - <P nativeID="dialog-description"> - {stage === Stages.ConfirmCode ? ( - <Trans> - An email has been sent to{' '} - {currentAccount?.email || '(no email)'}. It includes a - confirmation code which you can enter below. - </Trans> - ) : ( - <Trans> - To disable the email 2FA method, please verify your access to - the email address. - </Trans> - )} - </P> - - {error ? <ErrorMessage message={error} /> : undefined} - - {stage === Stages.Email ? ( - <View - style={[ - a.gap_sm, - gtMobile && [a.flex_row, a.justify_end, a.gap_md], - ]}> - <Button - testID="sendEmailButton" - variant="solid" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onSendEmail} - label={_(msg`Send verification email`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Send verification email</Trans> - </ButtonText> - {isProcessing && <ButtonIcon icon={Loader} />} - </Button> - <Button - testID="haveCodeButton" - variant="ghost" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={() => setStage(Stages.ConfirmCode)} - label={_(msg`I have a code`)} - disabled={isProcessing}> - <ButtonText> - <Trans>I have a code</Trans> - </ButtonText> - </Button> - </View> - ) : stage === Stages.ConfirmCode ? ( - <View> - <View style={[a.mb_md]}> - <TextField.LabelText> - <Trans>Confirmation code</Trans> - </TextField.LabelText> - <TextField.Root> - <TextField.Icon icon={Lock} /> - <Dialog.Input - testID="confirmationCode" - label={_(msg`Confirmation code`)} - autoCapitalize="none" - autoFocus - autoCorrect={false} - autoComplete="off" - value={confirmationCode} - onChangeText={setConfirmationCode} - onSubmitEditing={onConfirmDisable} - editable={!isProcessing} - /> - </TextField.Root> - </View> - <View - style={[ - a.gap_sm, - gtMobile && [a.flex_row, a.justify_end, a.gap_md], - ]}> - <Button - testID="resendCodeBtn" - variant="ghost" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onSendEmail} - label={_(msg`Resend email`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Resend email</Trans> - </ButtonText> - </Button> - <Button - testID="confirmBtn" - variant="solid" - color="primary" - size={gtMobile ? 'small' : 'large'} - onPress={onConfirmDisable} - label={_(msg`Confirm`)} - disabled={isProcessing}> - <ButtonText> - <Trans>Confirm</Trans> - </ButtonText> - {isProcessing && <ButtonIcon icon={Loader} />} - </Button> - </View> - </View> - ) : undefined} - - {!gtMobile && isNative && <View style={{height: 40}} />} - </View> - </Dialog.ScrollableInner> - </Dialog.Outer> - ) -} diff --git a/src/view/screens/Settings/Email2FAToggle.tsx b/src/view/screens/Settings/Email2FAToggle.tsx deleted file mode 100644 index f6ed19a21..000000000 --- a/src/view/screens/Settings/Email2FAToggle.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {useModalControls} from '#/state/modals' -import {useAgent, useSession} from '#/state/session' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {useDialogControl} from '#/components/Dialog' -import {DisableEmail2FADialog} from './DisableEmail2FADialog' - -export function Email2FAToggle() { - const {_} = useLingui() - const {currentAccount} = useSession() - const {openModal} = useModalControls() - const disableDialogCtrl = useDialogControl() - const agent = useAgent() - - const enableEmailAuthFactor = React.useCallback(async () => { - if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ - email: currentAccount.email, - emailAuthFactor: true, - }) - await agent.resumeSession(agent.session!) - } - }, [currentAccount, agent]) - - const onToggle = React.useCallback(() => { - if (!currentAccount) { - return - } - if (currentAccount.emailAuthFactor) { - disableDialogCtrl.open() - } else { - if (!currentAccount.emailConfirmed) { - openModal({ - name: 'verify-email', - onSuccess: enableEmailAuthFactor, - }) - return - } - enableEmailAuthFactor() - } - }, [currentAccount, enableEmailAuthFactor, openModal, disableDialogCtrl]) - - return ( - <> - <DisableEmail2FADialog control={disableDialogCtrl} /> - <ToggleButton - type="default-light" - label={_(msg`Require email code to log into your account`)} - labelType="lg" - isSelected={!!currentAccount?.emailAuthFactor} - onPress={onToggle} - /> - </> - ) -} diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/view/screens/Settings/ExportCarDialog.tsx deleted file mode 100644 index 2de3895d3..000000000 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' - -import {saveBytesToDisk} from '#/lib/media/manip' -import {logger} from '#/logger' -import {useAgent} from '#/state/session' -import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' -import {InlineLinkText} from '#/components/Link' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -export function ExportCarDialog({ - control, -}: { - control: Dialog.DialogOuterProps['control'] -}) { - const {_} = useLingui() - const t = useTheme() - const agent = useAgent() - const [loading, setLoading] = React.useState(false) - - const download = React.useCallback(async () => { - if (!agent.session) { - return // shouldnt ever happen - } - try { - setLoading(true) - const did = agent.session.did - const downloadRes = await agent.com.atproto.sync.getRepo({did}) - const saveRes = await saveBytesToDisk( - 'repo.car', - downloadRes.data, - downloadRes.headers['content-type'], - ) - - if (saveRes) { - Toast.show(_(msg`File saved successfully!`)) - } - } catch (e) { - logger.error('Error occurred while downloading CAR file', {message: e}) - Toast.show(_(msg`Error occurred while saving file`), 'xmark') - } finally { - setLoading(false) - control.close() - } - }, [_, control, agent]) - - return ( - <Dialog.Outer control={control}> - <Dialog.Handle /> - <Dialog.ScrollableInner - accessibilityDescribedBy="dialog-description" - accessibilityLabelledBy="dialog-title"> - <View style={[a.relative, a.gap_lg, a.w_full]}> - <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> - <Trans>Export My Data</Trans> - </Text> - <Text nativeID="dialog-description" style={[a.text_sm]}> - <Trans> - Your account repository, containing all public data records, can - be downloaded as a "CAR" file. This file does not include media - embeds, such as images, or your private data, which must be - fetched separately. - </Trans> - </Text> - - <Button - variant="solid" - color="primary" - size="large" - label={_(msg`Download CAR file`)} - disabled={loading} - onPress={download}> - <ButtonIcon icon={DownloadIcon} /> - <ButtonText> - <Trans>Download CAR file</Trans> - </ButtonText> - {loading && <ButtonIcon icon={Loader} />} - </Button> - - <Text - style={[ - t.atoms.text_contrast_medium, - a.text_sm, - a.leading_snug, - a.flex_1, - ]}> - <Trans> - This feature is in beta. You can read more about repository - exports in{' '} - <InlineLinkText - label={_(msg`View blogpost for more details`)} - to="https://docs.bsky.app/blog/repo-export" - style={[a.text_sm]}> - this blogpost - </InlineLinkText> - . - </Trans> - </Text> - </View> - </Dialog.ScrollableInner> - </Dialog.Outer> - ) -} diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx deleted file mode 100644 index 7ec7b5dce..000000000 --- a/src/view/screens/Settings/index.tsx +++ /dev/null @@ -1,1077 +0,0 @@ -import React from 'react' -import { - Platform, - Pressable, - StyleSheet, - TextStyle, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import {setStringAsync} from 'expo-clipboard' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useFocusEffect, useNavigation} from '@react-navigation/native' -import {useQueryClient} from '@tanstack/react-query' - -import {appVersion, BUNDLE_DATE, bundleInfo, IS_INTERNAL} from '#/lib/app-info' -import {STATUS_PAGE_URL} from '#/lib/constants' -import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' -import {useCustomPalette} from '#/lib/hooks/useCustomPalette' -import {usePalette} from '#/lib/hooks/usePalette' -import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' -import {HandIcon, HashtagIcon} from '#/lib/icons' -import {makeProfileLink} from '#/lib/routes/links' -import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' -import {NavigationProp} from '#/lib/routes/types' -import {colors, s} from '#/lib/styles' -import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' -import {clearStorage} from '#/state/persisted' -import { - useInAppBrowser, - useSetInAppBrowser, -} from '#/state/preferences/in-app-browser' -import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' -import {useClearPreferencesMutation} from '#/state/queries/preferences' -import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' -import {useProfileQuery} from '#/state/queries/profile' -import {SessionAccount, useSession, useSessionApi} from '#/state/session' -import {useOnboardingDispatch, useSetMinimalShellMode} from '#/state/shell' -import {useLoggedOutViewControls} from '#/state/shell/logged-out' -import {useCloseAllActiveElements} from '#/state/util' -import {AccountDropdownBtn} from '#/view/com/util/AccountDropdownBtn' -import {ToggleButton} from '#/view/com/util/forms/ToggleButton' -import {Link, TextLink} from '#/view/com/util/Link' -import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' -import {Text} from '#/view/com/util/text/Text' -import * as Toast from '#/view/com/util/Toast' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {ScrollView} from '#/view/com/util/Views' -import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog' -import {SettingsScreen as NewSettingsScreen} from '#/screens/Settings/Settings' -import {atoms as a, useTheme} from '#/alf' -import {useDialogControl} from '#/components/Dialog' -import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings' -import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog' -import * as Layout from '#/components/Layout' -import {Email2FAToggle} from './Email2FAToggle' -import {ExportCarDialog} from './ExportCarDialog' - -function SettingsAccountCard({ - account, - pendingDid, - onPressSwitchAccount, -}: { - account: SessionAccount - pendingDid: string | null - onPressSwitchAccount: ( - account: SessionAccount, - logContext: 'Settings', - ) => void -}) { - const pal = usePalette('default') - const {_} = useLingui() - const t = useTheme() - const {currentAccount} = useSession() - const {data: profile} = useProfileQuery({did: account.did}) - const isCurrentAccount = account.did === currentAccount?.did - - const contents = ( - <View - style={[ - pal.view, - styles.linkCard, - account.did === pendingDid && t.atoms.bg_contrast_25, - ]}> - <View style={styles.avi}> - <UserAvatar - size={40} - avatar={profile?.avatar} - type={profile?.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - <View style={[s.flex1]}> - <Text - emoji - type="md-bold" - style={[pal.text, a.self_start]} - numberOfLines={1}> - {profile?.displayName || account.handle} - </Text> - <Text emoji type="sm" style={pal.textLight} numberOfLines={1}> - {account.handle} - </Text> - </View> - <AccountDropdownBtn account={account} /> - </View> - ) - - return isCurrentAccount ? ( - <Link - href={makeProfileLink({ - did: currentAccount?.did, - handle: currentAccount?.handle, - })} - title={_(msg`Your profile`)} - noFeedback> - {contents} - </Link> - ) : ( - <TouchableOpacity - testID={`switchToAccountBtn-${account.handle}`} - key={account.did} - onPress={ - pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Switch to ${account.handle}`)} - accessibilityHint={_(msg`Switches the account you are logged in to`)} - activeOpacity={0.8}> - {contents} - </TouchableOpacity> - ) -} - -type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'> -export function SettingsScreen(props: Props) { - return IS_INTERNAL ? ( - <NewSettingsScreen {...props} /> - ) : ( - <LegacySettingsScreen {...props} /> - ) -} - -function LegacySettingsScreen({}: Props) { - const queryClient = useQueryClient() - const pal = usePalette('default') - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const inAppBrowserPref = useInAppBrowser() - const setUseInAppBrowser = useSetInAppBrowser() - const onboardingDispatch = useOnboardingDispatch() - const navigation = useNavigation<NavigationProp>() - const {isMobile} = useWebMediaQueries() - const {openModal} = useModalControls() - const {accounts, currentAccount} = useSession() - const {mutate: clearPreferences} = useClearPreferencesMutation() - const {setShowLoggedOut} = useLoggedOutViewControls() - const {logoutEveryAccount} = useSessionApi() - const closeAllActiveElements = useCloseAllActiveElements() - const exportCarControl = useDialogControl() - const birthdayControl = useDialogControl() - const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() - const isSwitchingAccounts = !!pendingDid - - // const primaryBg = useCustomPalette<ViewStyle>({ - // light: {backgroundColor: colors.blue0}, - // dark: {backgroundColor: colors.blue6}, - // }) - // const primaryText = useCustomPalette<TextStyle>({ - // light: {color: colors.blue3}, - // dark: {color: colors.blue2}, - // }) - - const dangerBg = useCustomPalette<ViewStyle>({ - light: {backgroundColor: colors.red1}, - dark: {backgroundColor: colors.red7}, - }) - const dangerText = useCustomPalette<TextStyle>({ - light: {color: colors.red4}, - dark: {color: colors.red2}, - }) - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onPressAddAccount = React.useCallback(() => { - setShowLoggedOut(true) - closeAllActiveElements() - }, [setShowLoggedOut, closeAllActiveElements]) - - const onPressChangeHandle = React.useCallback(() => { - openModal({ - name: 'change-handle', - onChanged() { - if (currentAccount) { - // refresh my profile - queryClient.invalidateQueries({ - queryKey: RQKEY_PROFILE(currentAccount.did), - }) - } - }, - }) - }, [queryClient, openModal, currentAccount]) - - const onPressExportRepository = React.useCallback(() => { - exportCarControl.open() - }, [exportCarControl]) - - const onPressLanguageSettings = React.useCallback(() => { - navigation.navigate('LanguageSettings') - }, [navigation]) - - const onPressDeleteAccount = React.useCallback(() => { - openModal({name: 'delete-account'}) - }, [openModal]) - - const onPressLogoutEveryAccount = React.useCallback(() => { - logoutEveryAccount('Settings') - }, [logoutEveryAccount]) - - const onPressResetPreferences = React.useCallback(async () => { - clearPreferences() - }, [clearPreferences]) - - const onPressResetOnboarding = React.useCallback(async () => { - navigation.navigate('Home') - onboardingDispatch({type: 'start'}) - Toast.show(_(msg`Onboarding reset`)) - }, [navigation, onboardingDispatch, _]) - - const onPressBuildInfo = React.useCallback(() => { - setStringAsync( - `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}`, - ) - Toast.show(_(msg`Copied build version to clipboard`)) - }, [_]) - - const openFollowingFeedPreferences = React.useCallback(() => { - navigation.navigate('PreferencesFollowingFeed') - }, [navigation]) - - const openThreadsPreferences = React.useCallback(() => { - navigation.navigate('PreferencesThreads') - }, [navigation]) - - const onPressAppPasswords = React.useCallback(() => { - navigation.navigate('AppPasswords') - }, [navigation]) - - const onPressSystemLog = React.useCallback(() => { - navigation.navigate('Log') - }, [navigation]) - - const onPressStorybook = React.useCallback(() => { - navigation.navigate('Debug') - }, [navigation]) - - const onPressDebugModeration = React.useCallback(() => { - navigation.navigate('DebugMod') - }, [navigation]) - - const onPressSavedFeeds = React.useCallback(() => { - navigation.navigate('SavedFeeds') - }, [navigation]) - - const onPressAccessibilitySettings = React.useCallback(() => { - navigation.navigate('AccessibilitySettings') - }, [navigation]) - - const onPressAppearanceSettings = React.useCallback(() => { - navigation.navigate('AppearanceSettings') - }, [navigation]) - - const onPressBirthday = React.useCallback(() => { - birthdayControl.open() - }, [birthdayControl]) - - const clearAllStorage = React.useCallback(async () => { - await clearStorage() - Toast.show(_(msg`Storage cleared, you need to restart the app now.`)) - }, [_]) - - const deactivateAccountControl = useDialogControl() - const onPressDeactivateAccount = React.useCallback(() => { - deactivateAccountControl.open() - }, [deactivateAccountControl]) - - const {mutate: onPressDeleteChatDeclaration} = useDeleteActorDeclaration() - - return ( - <Layout.Screen testID="settingsScreen"> - <ExportCarDialog control={exportCarControl} /> - <BirthDateSettingsDialog control={birthdayControl} /> - - <SimpleViewHeader - showBackButton={isMobile} - style={[ - pal.border, - {borderBottomWidth: StyleSheet.hairlineWidth}, - !isMobile && {borderLeftWidth: 1, borderRightWidth: 1}, - ]}> - <View style={{flex: 1}}> - <Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}> - <Trans>Settings</Trans> - </Text> - </View> - </SimpleViewHeader> - <ScrollView - style={[isMobile && pal.viewLight]} - scrollIndicatorInsets={{right: 1}} - // @ts-ignore web only -prf - dataSet={{'stable-gutters': 1}}> - <View style={styles.spacer20} /> - {currentAccount ? ( - <> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Account</Trans> - </Text> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Email:</Trans>{' '} - </Text> - {currentAccount.emailConfirmed && ( - <> - <FontAwesomeIcon - icon="check" - size={10} - style={{color: colors.green3, marginRight: 2}} - /> - </> - )} - <Text - type="lg" - numberOfLines={1} - style={[ - pal.text, - {overflow: 'hidden', marginRight: 4, flex: 1}, - ]}> - {currentAccount.email || '(no email)'} - </Text> - <Link onPress={() => openModal({name: 'change-email'})}> - <Text type="lg" style={pal.link}> - <Trans context="action">Change</Trans> - </Text> - </Link> - </View> - <View style={[styles.infoLine]}> - <Text type="lg-medium" style={pal.text}> - <Trans>Birthday:</Trans>{' '} - </Text> - <Link onPress={onPressBirthday}> - <Text type="lg" style={pal.link}> - <Trans>Show</Trans> - </Text> - </Link> - </View> - <View style={styles.spacer20} /> - - {!currentAccount.emailConfirmed && <EmailConfirmationNotice />} - - <View style={[s.flexRow, styles.heading]}> - <Text type="xl-bold" style={pal.text} numberOfLines={1}> - <Trans>Signed in as</Trans> - </Text> - <View style={s.flex1} /> - </View> - <View pointerEvents={pendingDid ? 'none' : 'auto'}> - <SettingsAccountCard - account={currentAccount} - onPressSwitchAccount={onPressSwitchAccount} - pendingDid={pendingDid} - /> - </View> - </> - ) : null} - - <View pointerEvents={pendingDid ? 'none' : 'auto'}> - {accounts.length > 1 && ( - <View style={[s.flexRow, styles.heading, a.mt_sm]}> - <Text type="xl-bold" style={pal.text} numberOfLines={1}> - <Trans>Other accounts</Trans> - </Text> - <View style={s.flex1} /> - </View> - )} - - {accounts - .filter(a => a.did !== currentAccount?.did) - .map(account => ( - <SettingsAccountCard - key={account.did} - account={account} - onPressSwitchAccount={onPressSwitchAccount} - pendingDid={pendingDid} - /> - ))} - - <TouchableOpacity - testID="switchToNewAccountBtn" - style={[styles.linkCard, pal.view]} - onPress={isSwitchingAccounts ? undefined : onPressAddAccount} - accessibilityRole="button" - accessibilityLabel={_(msg`Add account`)} - accessibilityHint={_(msg`Create a new Bluesky account`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="plus" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Add account</Trans> - </Text> - </TouchableOpacity> - - <TouchableOpacity - style={[styles.linkCard, pal.view]} - onPress={ - isSwitchingAccounts ? undefined : onPressLogoutEveryAccount - } - accessibilityRole="button" - accessibilityLabel={_(msg`Sign out of all accounts`)} - accessibilityHint={undefined}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="arrow-right-from-bracket" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - {accounts.length > 1 ? ( - <Trans>Sign out of all accounts</Trans> - ) : ( - <Trans>Sign out</Trans> - )} - </Text> - </TouchableOpacity> - </View> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Basics</Trans> - </Text> - <TouchableOpacity - testID="accessibilitySettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts ? undefined : onPressAccessibilitySettings - } - accessibilityRole="button" - accessibilityLabel={_(msg`Accessibility settings`)} - accessibilityHint={_(msg`Opens accessibility settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="universal-access" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Accessibility</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="appearanceSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressAppearanceSettings} - accessibilityRole="button" - accessibilityLabel={_(msg`Appearance settings`)} - accessibilityHint={_(msg`Opens appearance settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="paint-roller" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Appearance</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="languageSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings} - accessibilityRole="button" - accessibilityLabel={_(msg`Language settings`)} - accessibilityHint={_(msg`Opens configurable language settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="language" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Languages</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="moderationBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('Moderation') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Moderation settings`)} - accessibilityHint={_(msg`Opens moderation settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <HandIcon style={pal.text} size={18} strokeWidth={6} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Moderation</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="preferencesHomeFeedButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openFollowingFeedPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Following feed preferences`)} - accessibilityHint={_(msg`Opens the Following feed preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="sliders" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Following Feed Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="preferencesThreadsButton" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={openThreadsPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Thread preferences`)} - accessibilityHint={_(msg`Opens the threads preferences`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'comments']} - style={pal.text as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Thread Preferences</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="savedFeedsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={onPressSavedFeeds} - accessibilityRole="button" - accessibilityLabel={_(msg`My saved feeds`)} - accessibilityHint={_(msg`Opens screen with all saved feeds`)}> - <View style={[styles.iconContainer, pal.btn]}> - <HashtagIcon style={pal.text} size={18} strokeWidth={3} /> - </View> - <Text type="lg" style={pal.text}> - <Trans>My Saved Feeds</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="linkToChatSettingsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('MessagesSettings') - } - accessibilityRole="button" - accessibilityLabel={_(msg`Chat settings`)} - accessibilityHint={_(msg`Opens chat settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'comment-dots']} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>Chat Settings</Trans> - </Text> - </TouchableOpacity> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Privacy</Trans> - </Text> - - <TouchableOpacity - testID="externalEmbedsBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={ - isSwitchingAccounts - ? undefined - : () => navigation.navigate('PreferencesExternalEmbeds') - } - accessibilityRole="button" - accessibilityLabel={_(msg`External media settings`)} - accessibilityHint={_(msg`Opens external embeds settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'circle-play']} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>External Media Preferences</Trans> - </Text> - </TouchableOpacity> - - <View style={styles.spacer20} /> - - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Advanced</Trans> - </Text> - <TouchableOpacity - testID="appPasswordBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={onPressAppPasswords} - accessibilityRole="button" - accessibilityLabel={_(msg`App password settings`)} - accessibilityHint={_(msg`Opens the app password settings`)}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - <Trans>App Passwords</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="changeHandleBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressChangeHandle} - accessibilityRole="button" - accessibilityLabel={_(msg`Change handle`)} - accessibilityHint={_( - msg`Opens modal for choosing a new Bluesky handle`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="at" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Change Handle</Trans> - </Text> - </TouchableOpacity> - {isNative && ( - <View style={[pal.view, styles.toggleCard]}> - <ToggleButton - type="default-light" - label={_(msg`Open links with in-app browser`)} - labelType="lg" - isSelected={inAppBrowserPref ?? false} - onPress={() => setUseInAppBrowser(!inAppBrowserPref)} - /> - </View> - )} - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Two-factor authentication</Trans> - </Text> - <View style={[pal.view, styles.toggleCard]}> - <Email2FAToggle /> - </View> - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Account</Trans> - </Text> - <TouchableOpacity - testID="changePasswordBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={() => openModal({name: 'change-password'})} - accessibilityRole="button" - accessibilityLabel={_(msg`Change password`)} - accessibilityHint={_( - msg`Opens modal for changing your Bluesky password`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="lock" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Change Password</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="exportRepositoryBtn" - style={[ - styles.linkCard, - pal.view, - isSwitchingAccounts && styles.dimmed, - ]} - onPress={isSwitchingAccounts ? undefined : onPressExportRepository} - accessibilityRole="button" - accessibilityLabel={_(msg`Export my data`)} - accessibilityHint={_( - msg`Opens modal for downloading your Bluesky account data (repository)`, - )}> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="download" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text} numberOfLines={1}> - <Trans>Export My Data</Trans> - </Text> - </TouchableOpacity> - - <TouchableOpacity - style={[pal.view, styles.linkCard]} - onPress={onPressDeactivateAccount} - accessible={true} - accessibilityRole="button" - accessibilityLabel={_(msg`Deactivate account`)} - accessibilityHint={_( - msg`Opens modal for account deactivation confirmation`, - )}> - <View style={[styles.iconContainer, dangerBg]}> - <FontAwesomeIcon - icon={'users-slash'} - style={dangerText as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={dangerText}> - <Trans>Deactivate my account</Trans> - </Text> - </TouchableOpacity> - <DeactivateAccountDialog control={deactivateAccountControl} /> - - <TouchableOpacity - style={[pal.view, styles.linkCard]} - onPress={onPressDeleteAccount} - accessible={true} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete account`)} - accessibilityHint={_( - msg`Opens modal for account deletion confirmation. Requires email code`, - )}> - <View style={[styles.iconContainer, dangerBg]}> - <FontAwesomeIcon - icon={['far', 'trash-can']} - style={dangerText as FontAwesomeIconStyle} - size={18} - /> - </View> - <Text type="lg" style={dangerText}> - <Trans>Delete My Account…</Trans> - </Text> - </TouchableOpacity> - <View style={styles.spacer20} /> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressSystemLog} - accessibilityRole="button" - accessibilityLabel={_(msg`Open system log`)} - accessibilityHint={_(msg`Opens the system log page`)}> - <Text type="lg" style={pal.text}> - <Trans>System log</Trans> - </Text> - </TouchableOpacity> - {__DEV__ ? ( - <> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressStorybook} - accessibilityRole="button" - accessibilityLabel={_(msg`Open storybook page`)} - accessibilityHint={_(msg`Opens the storybook page`)}> - <Text type="lg" style={pal.text}> - <Trans>Storybook</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressDebugModeration} - accessibilityRole="button" - accessibilityLabel={_(msg`Open storybook page`)} - accessibilityHint={_(msg`Opens the storybook page`)}> - <Text type="lg" style={pal.text}> - <Trans>Debug Moderation</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetPreferences} - accessibilityRole="button" - accessibilityLabel={_(msg`Reset preferences state`)} - accessibilityHint={_(msg`Resets the preferences state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset preferences state</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={() => onPressDeleteChatDeclaration()} - accessibilityRole="button" - accessibilityLabel={_(msg`Delete chat declaration record`)} - accessibilityHint={_(msg`Deletes the chat declaration record`)}> - <Text type="lg" style={pal.text}> - <Trans>Delete chat declaration record</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={onPressResetOnboarding} - accessibilityRole="button" - accessibilityLabel={_(msg`Reset onboarding state`)} - accessibilityHint={_(msg`Resets the onboarding state`)}> - <Text type="lg" style={pal.text}> - <Trans>Reset onboarding state</Trans> - </Text> - </TouchableOpacity> - <TouchableOpacity - style={[pal.view, styles.linkCardNoIcon]} - onPress={clearAllStorage} - accessibilityRole="button" - accessibilityLabel={_(msg`Clear all storage data`)} - accessibilityHint={_(msg`Clears all storage data`)}> - <Text type="lg" style={pal.text}> - <Trans>Clear all storage data (restart after this)</Trans> - </Text> - </TouchableOpacity> - </> - ) : null} - <View style={[styles.footer]}> - <TouchableOpacity - accessibilityRole="button" - onPress={onPressBuildInfo}> - <Text type="sm" style={[styles.buildInfo, pal.textLight]}> - <Trans> - Version {appVersion} {bundleInfo} - </Trans> - </Text> - </TouchableOpacity> - </View> - - <View - style={[ - {flexWrap: 'wrap', gap: 12, paddingHorizontal: 18}, - s.flexRow, - ]}> - <TextLink - type="md" - style={pal.link} - href="https://bsky.social/about/support/tos" - text={_(msg`Terms of Service`)} - /> - <TextLink - type="md" - style={pal.link} - href="https://bsky.social/about/support/privacy-policy" - text={_(msg`Privacy Policy`)} - /> - <TextLink - type="md" - style={pal.link} - href={STATUS_PAGE_URL} - text={_(msg`Status Page`)} - /> - </View> - <View style={s.footerSpacer} /> - </ScrollView> - </Layout.Screen> - ) -} - -function EmailConfirmationNotice() { - const pal = usePalette('default') - const palInverted = usePalette('inverted') - const {_} = useLingui() - const {isMobile} = useWebMediaQueries() - const verifyEmailDialogControl = useDialogControl() - - return ( - <View style={{marginBottom: 20}}> - <Text type="xl-bold" style={[pal.text, styles.heading]}> - <Trans>Verify email</Trans> - </Text> - <View - style={[ - { - paddingVertical: isMobile ? 12 : 0, - paddingHorizontal: 18, - }, - pal.view, - ]}> - <View style={{flexDirection: 'row', marginBottom: 8}}> - <Pressable - style={[ - palInverted.view, - { - flexDirection: 'row', - gap: 6, - borderRadius: 6, - paddingHorizontal: 12, - paddingVertical: 10, - alignItems: 'center', - }, - isMobile && {flex: 1}, - ]} - accessibilityRole="button" - accessibilityLabel={_(msg`Verify my email`)} - accessibilityHint={_(msg`Opens modal for email verification`)} - onPress={() => verifyEmailDialogControl.open()}> - <FontAwesomeIcon - icon="envelope" - color={palInverted.colors.text} - size={16} - /> - <Text type="button" style={palInverted.text}> - <Trans>Verify My Email</Trans> - </Text> - </Pressable> - </View> - <Text style={pal.textLight}> - <Trans>Protect your account by verifying your email.</Trans> - </Text> - </View> - <VerifyEmailDialog control={verifyEmailDialogControl} /> - </View> - ) -} - -const styles = StyleSheet.create({ - dimmed: { - opacity: 0.5, - }, - spacer20: { - height: 20, - }, - heading: { - paddingHorizontal: 18, - paddingBottom: 6, - }, - infoLine: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 18, - paddingBottom: 6, - }, - profile: { - flexDirection: 'row', - marginVertical: 6, - borderRadius: 4, - paddingVertical: 10, - paddingHorizontal: 10, - }, - linkCard: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 12, - paddingHorizontal: 18, - marginBottom: 1, - }, - linkCardNoIcon: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 20, - paddingHorizontal: 18, - marginBottom: 1, - }, - toggleCard: { - paddingVertical: 8, - paddingHorizontal: 6, - marginBottom: 1, - }, - avi: { - marginRight: 12, - }, - iconContainer: { - alignItems: 'center', - justifyContent: 'center', - width: 40, - height: 40, - borderRadius: 30, - marginRight: 12, - }, - buildInfo: { - paddingVertical: 8, - }, - - colorModeText: { - marginLeft: 10, - marginBottom: 6, - }, - - selectableBtns: { - flexDirection: 'row', - }, - - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - width: '100%', - borderRadius: 32, - padding: 14, - backgroundColor: colors.gray1, - }, - toggleBtn: { - paddingHorizontal: 0, - }, - footer: { - flex: 1, - flexDirection: 'row', - paddingLeft: 18, - }, -}) |