diff options
Diffstat (limited to 'src')
22 files changed, 16 insertions, 3768 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index efe4b8c29..0ab4bb613 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -40,14 +40,11 @@ import { shouldRequestEmailConfirmation, snoozeEmailConfirmationPrompt, } from '#/state/shell/reminders' -import {AccessibilitySettingsScreen} from '#/view/screens/AccessibilitySettings' -import {AppPasswords} from '#/view/screens/AppPasswords' import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines' import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy' import {DebugModScreen} from '#/view/screens/DebugMod' import {FeedsScreen} from '#/view/screens/Feeds' import {HomeScreen} from '#/view/screens/Home' -import {LanguageSettingsScreen} from '#/view/screens/LanguageSettings' import {ListsScreen} from '#/view/screens/Lists' import {LogScreen} from '#/view/screens/Log' import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts' @@ -56,9 +53,6 @@ import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts' import {NotFoundScreen} from '#/view/screens/NotFound' import {NotificationsScreen} from '#/view/screens/Notifications' import {PostThreadScreen} from '#/view/screens/PostThread' -import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds' -import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed' -import {PreferencesThreads} from '#/view/screens/PreferencesThreads' import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy' import {ProfileScreen} from '#/view/screens/Profile' import {ProfileFeedScreen} from '#/view/screens/ProfileFeed' @@ -68,7 +62,6 @@ import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows' import {ProfileListScreen} from '#/view/screens/ProfileList' import {SavedFeeds} from '#/view/screens/SavedFeeds' import {SearchScreen} from '#/view/screens/Search' -import {SettingsScreen} from '#/view/screens/Settings' import {Storybook} from '#/view/screens/Storybook' import {SupportScreen} from '#/view/screens/Support' import {TermsOfServiceScreen} from '#/view/screens/TermsOfService' @@ -96,9 +89,16 @@ import {useTheme} from '#/alf' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' import {AboutSettingsScreen} from './screens/Settings/AboutSettings' +import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings' import {AccountSettingsScreen} from './screens/Settings/AccountSettings' +import {AppPasswordsScreen} from './screens/Settings/AppPasswords' import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings' +import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences' +import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences' +import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' +import {SettingsScreen} from './screens/Settings/Settings' +import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -285,7 +285,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> <Stack.Screen name="AppPasswords" - getComponent={() => AppPasswords} + getComponent={() => AppPasswordsScreen} options={{title: title(msg`App Passwords`), requireAuth: true}} /> <Stack.Screen @@ -295,7 +295,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> <Stack.Screen name="PreferencesFollowingFeed" - getComponent={() => PreferencesFollowingFeed} + getComponent={() => FollowingFeedPreferencesScreen} options={{ title: title(msg`Following Feed Preferences`), requireAuth: true, @@ -303,12 +303,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { /> <Stack.Screen name="PreferencesThreads" - getComponent={() => PreferencesThreads} + getComponent={() => ThreadPreferencesScreen} options={{title: title(msg`Threads Preferences`), requireAuth: true}} /> <Stack.Screen name="PreferencesExternalEmbeds" - getComponent={() => PreferencesExternalEmbeds} + getComponent={() => ExternalMediaPreferencesScreen} options={{ title: title(msg`External Media Preferences`), requireAuth: true, diff --git a/src/lib/hooks/useCustomPalette.ts b/src/lib/hooks/useCustomPalette.ts deleted file mode 100644 index 5691ea79b..000000000 --- a/src/lib/hooks/useCustomPalette.ts +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' - -import {choose} from '#/lib/functions' -import {useTheme} from '#/lib/ThemeContext' - -export function useCustomPalette<T>({light, dark}: {light: T; dark: T}) { - const theme = useTheme() - return React.useMemo(() => { - return choose<T, Record<string, T>>(theme.colorScheme, { - dark, - light, - }) - }, [theme.colorScheme, dark, light]) -} diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index d5a2daffd..5f340cd56 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -1,13 +1,11 @@ import React from 'react' import {Linking, View} from 'react-native' import {useSafeAreaFrame} from 'react-native-safe-area-context' -import {ComAtprotoLabelDefs} from '@atproto/api' import {LABELS} from '@atproto/api' 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 {getLabelingServiceTitle} from '#/lib/moderation' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {logger} from '#/logger' @@ -18,11 +16,6 @@ import { UsePreferencesQueryResponse, usePreferencesSetAdultContentMutation, } from '#/state/queries/preferences' -import { - useProfileQuery, - useProfileUpdateMutation, -} from '#/state/queries/profile' -import {useSession} from '#/state/session' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useSetMinimalShellMode} from '#/state/shell' import {ViewHeader} from '#/view/com/util/ViewHeader' @@ -469,131 +462,7 @@ export function ModerationScreenInner({ })} </View> )} - - {!IS_INTERNAL && ( - <> - <Text - style={[ - a.text_md, - a.font_bold, - a.pt_2xl, - a.pb_md, - t.atoms.text_contrast_high, - ]}> - <Trans>Logged-out visibility</Trans> - </Text> - - <PwiOptOut /> - </> - )} - <View style={{height: 200}} /> </ScrollView> ) } - -function PwiOptOut() { - const t = useTheme() - const {_} = useLingui() - const {currentAccount} = useSession() - const {data: profile} = useProfileQuery({did: currentAccount?.did}) - const updateProfile = useProfileUpdateMutation() - - const isOptedOut = - profile?.labels?.some(l => l.val === '!no-unauthenticated') || false - const canToggle = profile && !updateProfile.isPending - - const onToggleOptOut = React.useCallback(() => { - if (!profile) { - return - } - let wasAdded = false - updateProfile.mutate({ - profile, - updates: existing => { - // create labels attr if needed - existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) - ? existing.labels - : { - $type: 'com.atproto.label.defs#selfLabels', - values: [], - } - - // toggle the label - const hasLabel = existing.labels.values.some( - l => l.val === '!no-unauthenticated', - ) - if (hasLabel) { - wasAdded = false - existing.labels.values = existing.labels.values.filter( - l => l.val !== '!no-unauthenticated', - ) - } else { - wasAdded = true - existing.labels.values.push({val: '!no-unauthenticated'}) - } - - // delete if no longer needed - if (existing.labels.values.length === 0) { - delete existing.labels - } - return existing - }, - checkCommitted: res => { - const exists = !!res.data.labels?.some( - l => l.val === '!no-unauthenticated', - ) - return exists === wasAdded - }, - }) - }, [updateProfile, profile]) - - return ( - <View style={[a.pt_sm]}> - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> - <Toggle.Item - disabled={!canToggle} - value={isOptedOut} - onChange={onToggleOptOut} - name="logged_out_visibility" - style={a.flex_1} - label={_( - msg`Discourage apps from showing my account to logged-out users`, - )}> - <Toggle.Switch /> - <Toggle.LabelText style={[a.text_md, a.flex_1]}> - <Trans> - Discourage apps from showing my account to logged-out users - </Trans> - </Toggle.LabelText> - </Toggle.Item> - - {updateProfile.isPending && <Loader />} - </View> - - <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}> - <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> - <Trans> - Bluesky will not show your profile and posts to logged-out users. - Other apps may not honor this request. This does not make your - account private. - </Trans> - </Text> - <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> - <Trans> - Note: Bluesky is an open and public network. This setting only - limits the visibility of your content on the Bluesky app and - website, and other apps may not respect this setting. Your content - may still be shown to logged-out users by other apps and websites. - </Trans> - </Text> - - <InlineLinkText - label={_(msg`Learn more about what is public on Bluesky.`)} - to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> - <Trans>Learn more about what is public on Bluesky.</Trans> - </InlineLinkText> - </View> - </View> - ) -} diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index f34810a68..35c5f3aa0 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -6,7 +6,6 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from '#/lib/routes/types' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' -import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -24,6 +23,7 @@ import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/ico import * as Layout from '#/components/Layout' import {ChangeHandleDialog} from './components/ChangeHandleDialog' import {DeactivateAccountDialog} from './components/DeactivateAccountDialog' +import {ExportCarDialog} from './components/ExportCarDialog' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'> export function AccountSettingsScreen({}: Props) { diff --git a/src/view/screens/Settings/DisableEmail2FADialog.tsx b/src/screens/Settings/components/DisableEmail2FADialog.tsx index 1378759b0..1378759b0 100644 --- a/src/view/screens/Settings/DisableEmail2FADialog.tsx +++ b/src/screens/Settings/components/DisableEmail2FADialog.tsx diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx index 85ae89dea..a74f9fce7 100644 --- a/src/screens/Settings/components/Email2FAToggle.tsx +++ b/src/screens/Settings/components/Email2FAToggle.tsx @@ -4,9 +4,9 @@ import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' import {useAgent, useSession} from '#/state/session' -import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog' import {useDialogControl} from '#/components/Dialog' import * as Prompt from '#/components/Prompt' +import {DisableEmail2FADialog} from './DisableEmail2FADialog' import * as SettingsList from './SettingsList' export function Email2FAToggle() { diff --git a/src/view/screens/Settings/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index 2de3895d3..2de3895d3 100644 --- a/src/view/screens/Settings/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 78f476d52..483de99e4 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -48,11 +48,6 @@ export interface DeleteAccountModal { name: 'delete-account' } -export interface ChangeHandleModal { - name: 'change-handle' - onChanged: () => void -} - export interface WaitlistModal { name: 'waitlist' } @@ -61,10 +56,6 @@ export interface InviteCodesModal { name: 'invite-codes' } -export interface AddAppPasswordModal { - name: 'add-app-password' -} - export interface ContentLanguagesSettingsModal { name: 'content-languages-settings' } @@ -101,8 +92,6 @@ export interface InAppBrowserConsentModal { export type Modal = // Account - | AddAppPasswordModal - | ChangeHandleModal | DeleteAccountModal | VerifyEmailModal | ChangeEmailModal 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/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/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, - }, -}) |