Settings revamp (#5745)
* start building storybook * add title * add some styles * try out new icons * more settings list component parts * make text do the spacing * clean up storybook * gated new settings screen * switch account * add current profile * use Layout.Screen * Layout.Header and Layout.Content * translate helpdesk text thanks @surfdude29! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add account settings * undo changes to export car dialog * privacy and security screen * Translate protect account stuff Thanks @surfdude29! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * content and media settings * about settings * 2fa copy Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * a11y and appearance * use new components for appearance settings * redesign accessibility settings * Update ContentAndMediaSettings.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add divider * remove a11y and appearance middleman screen * fix web settingslist styles * new SettingsList.Group component * explain how portal magic works * hide pwioptout in old location * Update Settings.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * replace gate with `IS_INTERNAL` * add IS_INTERNAL to app-info.web * fix profile area growing * add close button to switch account --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ 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'
|
||||
@@ -469,18 +470,22 @@ export function ModerationScreenInner({
|
||||
</View>
|
||||
)}
|
||||
|
||||
<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>
|
||||
{!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 />
|
||||
<PwiOptOut />
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={{height: 200}} />
|
||||
</ScrollView>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
|
||||
import {STATUS_PAGE_URL} from '#/lib/constants'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines'
|
||||
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||
import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper'
|
||||
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AboutSettings'>
|
||||
export function AboutSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`About`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.LinkItem
|
||||
to="https://bsky.social/about/support/tos"
|
||||
label={_(msg`Terms of Service`)}>
|
||||
<SettingsList.ItemIcon icon={NewspaperIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Terms of Service</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="https://bsky.social/about/support/privacy-policy"
|
||||
label={_(msg`Privacy Policy`)}>
|
||||
<SettingsList.ItemIcon icon={NewspaperIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Privacy Policy</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to={STATUS_PAGE_URL}
|
||||
label={_(msg`Status Page`)}>
|
||||
<SettingsList.ItemIcon icon={GlobeIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Status Page</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.LinkItem to="/sys/log" label={_(msg`System log`)}>
|
||||
<SettingsList.ItemIcon icon={CodeLinesIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>System log</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Version ${appVersion}`)}
|
||||
accessibilityHint={_(msg`Copy build version to clipboard`)}
|
||||
onPress={() => {
|
||||
setStringAsync(
|
||||
`Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}`,
|
||||
)
|
||||
Toast.show(_(msg`Copied build version to clipboard`))
|
||||
}}>
|
||||
<SettingsList.ItemIcon icon={WrenchIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Version {appVersion}</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText>
|
||||
</SettingsList.PressableItem>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
useHapticsDisabled,
|
||||
useRequireAltTextEnabled,
|
||||
useSetHapticsDisabled,
|
||||
useSetRequireAltTextEnabled,
|
||||
} from '#/state/preferences'
|
||||
import {
|
||||
useLargeAltBadgeEnabled,
|
||||
useSetLargeAltBadgeEnabled,
|
||||
} from '#/state/preferences/large-alt-badge'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'AccessibilitySettings'
|
||||
>
|
||||
export function AccessibilitySettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const requireAltTextEnabled = useRequireAltTextEnabled()
|
||||
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
|
||||
const hapticsDisabled = useHapticsDisabled()
|
||||
const setHapticsDisabled = useSetHapticsDisabled()
|
||||
const largeAltBadgeEnabled = useLargeAltBadgeEnabled()
|
||||
const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Accessibility`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Group contentContainerStyle={[a.gap_sm]}>
|
||||
<SettingsList.ItemIcon icon={AccessibilityIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Alt text</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Item
|
||||
name="require_alt_text"
|
||||
label={_(msg`Require alt text before posting`)}
|
||||
value={requireAltTextEnabled ?? false}
|
||||
onChange={value => setRequireAltTextEnabled(value)}
|
||||
style={[a.w_full]}>
|
||||
<Toggle.LabelText style={[a.flex_1]}>
|
||||
<Trans>Require alt text before posting</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Platform />
|
||||
</Toggle.Item>
|
||||
<Toggle.Item
|
||||
name="large_alt_badge"
|
||||
label={_(msg`Display larger alt text badges`)}
|
||||
value={!!largeAltBadgeEnabled}
|
||||
onChange={value => setLargeAltBadgeEnabled(value)}
|
||||
style={[a.w_full]}>
|
||||
<Toggle.LabelText style={[a.flex_1]}>
|
||||
<Trans>Display larger alt text badges</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Platform />
|
||||
</Toggle.Item>
|
||||
</SettingsList.Group>
|
||||
{isNative && (
|
||||
<>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.Group contentContainerStyle={[a.gap_sm]}>
|
||||
<SettingsList.ItemIcon icon={HapticIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Haptics</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Item
|
||||
name="haptics"
|
||||
label={_(msg`Disable haptic feedback`)}
|
||||
value={hapticsDisabled ?? false}
|
||||
onChange={value => setHapticsDisabled(value)}
|
||||
style={[a.w_full]}>
|
||||
<Toggle.LabelText style={[a.flex_1]}>
|
||||
<Trans>Disable haptic feedback</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Platform />
|
||||
</Toggle.Item>
|
||||
</SettingsList.Group>
|
||||
</>
|
||||
)}
|
||||
<SettingsList.Item>
|
||||
<Admonition type="info" style={[a.flex_1]}>
|
||||
<Trans>
|
||||
Autoplay options have moved to the{' '}
|
||||
<InlineLinkText
|
||||
to="/settings/content-and-media"
|
||||
label={_(msg`Content and media`)}>
|
||||
Content and Media settings
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</SettingsList.Item>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import React from 'react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
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'
|
||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
|
||||
import {BirthdayCake_Stroke2_Corner2_Rounded as BirthdayCakeIcon} from '#/components/icons/BirthdayCake'
|
||||
import {Car_Stroke2_Corner2_Rounded as CarIcon} from '#/components/icons/Car'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {Envelope_Stroke2_Corner2_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {Freeze_Stroke2_Corner2_Rounded as FreezeIcon} from '#/components/icons/Freeze'
|
||||
import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
|
||||
import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/icons/Pencil'
|
||||
import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash'
|
||||
import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
||||
export function AccountSettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const {openModal} = useModalControls()
|
||||
const birthdayControl = useDialogControl()
|
||||
const exportCarControl = useDialogControl()
|
||||
const deactivateAccountControl = useDialogControl()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Account`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={EnvelopeIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Email</Trans>
|
||||
</SettingsList.ItemText>
|
||||
{currentAccount && (
|
||||
<>
|
||||
<SettingsList.BadgeText>
|
||||
{currentAccount.email || <Trans>(no email)</Trans>}
|
||||
</SettingsList.BadgeText>
|
||||
{currentAccount.emailConfirmed ? (
|
||||
<CheckIcon color={t.palette.positive_500} size="sm" />
|
||||
) : (
|
||||
<SettingsList.BadgeButton
|
||||
label={_(msg`Verify`)}
|
||||
onPress={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SettingsList.Item>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Change email`)}
|
||||
onPress={() => openModal({name: 'change-email'})}>
|
||||
<SettingsList.ItemIcon icon={PencilIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Change email</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/privacy-and-security"
|
||||
label={_(msg`Protect your account`)}
|
||||
style={[
|
||||
a.my_xs,
|
||||
a.mx_lg,
|
||||
a.rounded_md,
|
||||
{backgroundColor: t.palette.primary_50},
|
||||
]}
|
||||
chevronColor={t.palette.primary_500}
|
||||
hoverStyle={[{backgroundColor: t.palette.primary_100}]}
|
||||
contentContainerStyle={[a.rounded_md, a.px_lg]}>
|
||||
<SettingsList.ItemIcon
|
||||
icon={VerifiedIcon}
|
||||
color={t.palette.primary_500}
|
||||
/>
|
||||
<SettingsList.ItemText
|
||||
style={[{color: t.palette.primary_500}, a.font_bold]}>
|
||||
<Trans>Protect your account</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={BirthdayCakeIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Birthday</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.BadgeButton
|
||||
label={_(msg`Edit`)}
|
||||
onPress={() => birthdayControl.open()}
|
||||
/>
|
||||
</SettingsList.Item>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Password`)}
|
||||
onPress={() => openModal({name: 'change-password'})}>
|
||||
<SettingsList.ItemIcon icon={LockIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Password</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Handle`)}
|
||||
onPress={() =>
|
||||
openModal({
|
||||
name: 'change-handle',
|
||||
onChanged() {
|
||||
if (currentAccount) {
|
||||
// refresh my profile
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: RQKEY_PROFILE(currentAccount.did),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}>
|
||||
<SettingsList.ItemIcon icon={AtIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Handle</Trans>
|
||||
</SettingsList.ItemText>
|
||||
{profile && (
|
||||
<SettingsList.BadgeText>@{profile.handle}</SettingsList.BadgeText>
|
||||
)}
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Export my data`)}
|
||||
onPress={() => exportCarControl.open()}>
|
||||
<SettingsList.ItemIcon icon={CarIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Export my data</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Deactivate account`)}
|
||||
onPress={() => deactivateAccountControl.open()}
|
||||
destructive>
|
||||
<SettingsList.ItemIcon icon={FreezeIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Deactivate account</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.PressableItem
|
||||
label={_(msg`Delete account`)}
|
||||
onPress={() => openModal({name: 'delete-account'})}
|
||||
destructive>
|
||||
<SettingsList.ItemIcon icon={Trash_Stroke2_Corner2_Rounded} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Delete account</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
<BirthDateSettingsDialog control={birthdayControl} />
|
||||
<ExportCarDialog control={exportCarControl} />
|
||||
<DeactivateAccountDialog control={deactivateAccountControl} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {
|
||||
FadeInDown,
|
||||
FadeOutDown,
|
||||
FadeInUp,
|
||||
FadeOutUp,
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||
import {ScrollView} from '#/view/com/util/Views'
|
||||
import {atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
@@ -22,12 +19,11 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T
|
||||
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as SettingsList from './components/SettingsList'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
|
||||
export function AppearanceSettingsScreen({}: Props) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
const {fonts} = useAlf()
|
||||
|
||||
const {colorMode, darkTheme} = useThemePrefs()
|
||||
@@ -77,66 +73,54 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
return (
|
||||
<LayoutAnimationConfig skipExiting skipEntering>
|
||||
<Layout.Screen testID="preferencesThreadsScreen">
|
||||
<ScrollView
|
||||
// @ts-ignore web only -prf
|
||||
dataSet={{'stable-gutters': 1}}
|
||||
contentContainerStyle={{paddingBottom: 75}}>
|
||||
<SimpleViewHeader
|
||||
showBackButton={isTabletOrMobile}
|
||||
style={[t.atoms.border_contrast_medium, a.border_b]}>
|
||||
<View style={a.flex_1}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Appearance</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<Layout.Header title={_(msg`Appearance`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Color mode`)}
|
||||
icon={PhoneIcon}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`System`),
|
||||
name: 'system',
|
||||
},
|
||||
{
|
||||
label: _(msg`Light`),
|
||||
name: 'light',
|
||||
},
|
||||
{
|
||||
label: _(msg`Dark`),
|
||||
name: 'dark',
|
||||
},
|
||||
]}
|
||||
values={[colorMode]}
|
||||
onChange={onChangeAppearance}
|
||||
/>
|
||||
|
||||
<View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Color mode`)}
|
||||
icon={PhoneIcon}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`System`),
|
||||
name: 'system',
|
||||
},
|
||||
{
|
||||
label: _(msg`Light`),
|
||||
name: 'light',
|
||||
},
|
||||
{
|
||||
label: _(msg`Dark`),
|
||||
name: 'dark',
|
||||
},
|
||||
]}
|
||||
values={[colorMode]}
|
||||
onChange={onChangeAppearance}
|
||||
/>
|
||||
|
||||
{colorMode !== 'light' && (
|
||||
<Animated.View
|
||||
entering={native(FadeInDown)}
|
||||
exiting={native(FadeOutDown)}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Dark theme`)}
|
||||
icon={MoonIcon}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`Dim`),
|
||||
name: 'dim',
|
||||
},
|
||||
{
|
||||
label: _(msg`Dark`),
|
||||
name: 'dark',
|
||||
},
|
||||
]}
|
||||
values={[darkTheme ?? 'dim']}
|
||||
onChange={onChangeDarkTheme}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
{colorMode !== 'light' && (
|
||||
<Animated.View
|
||||
entering={native(FadeInUp)}
|
||||
exiting={native(FadeOutUp)}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Dark theme`)}
|
||||
icon={MoonIcon}
|
||||
items={[
|
||||
{
|
||||
label: _(msg`Dim`),
|
||||
name: 'dim',
|
||||
},
|
||||
{
|
||||
label: _(msg`Dark`),
|
||||
name: 'dark',
|
||||
},
|
||||
]}
|
||||
values={[darkTheme ?? 'dim']}
|
||||
onChange={onChangeDarkTheme}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<Animated.View layout={native(LinearTransition)}>
|
||||
<AppearanceToggleButtonGroup
|
||||
title={_(msg`Font`)}
|
||||
description={_(
|
||||
@@ -177,9 +161,9 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
values={[fonts.scale]}
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
</LayoutAnimationConfig>
|
||||
)
|
||||
@@ -205,29 +189,32 @@ export function AppearanceToggleButtonGroup({
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View style={[a.gap_sm]}>
|
||||
<View style={[a.gap_xs]}>
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
<Icon style={t.atoms.text} />
|
||||
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
|
||||
</View>
|
||||
<>
|
||||
<SettingsList.Group contentContainerStyle={[a.gap_sm]} iconInset={false}>
|
||||
<SettingsList.ItemIcon icon={Icon} />
|
||||
<SettingsList.ItemText>{title}</SettingsList.ItemText>
|
||||
{description && (
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
style={[
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.w_full,
|
||||
]}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<ToggleButton.Group label={title} values={values} onChange={onChange}>
|
||||
{items.map(item => (
|
||||
<ToggleButton.Button
|
||||
key={item.name}
|
||||
label={item.label}
|
||||
name={item.name}>
|
||||
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
))}
|
||||
</ToggleButton.Group>
|
||||
</View>
|
||||
<ToggleButton.Group label={title} values={values} onChange={onChange}>
|
||||
{items.map(item => (
|
||||
<ToggleButton.Button
|
||||
key={item.name}
|
||||
label={item.label}
|
||||
name={item.name}>
|
||||
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
|
||||
</ToggleButton.Button>
|
||||
))}
|
||||
</ToggleButton.Group>
|
||||
</SettingsList.Group>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
|
||||
import {
|
||||
useInAppBrowser,
|
||||
useSetInAppBrowser,
|
||||
} from '#/state/preferences/in-app-browser'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
|
||||
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
||||
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
||||
import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
|
||||
import * as Layout from '#/components/Layout'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'ContentAndMediaSettings'
|
||||
>
|
||||
export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const autoplayDisabledPref = useAutoplayDisabled()
|
||||
const setAutoplayDisabledPref = useSetAutoplayDisabled()
|
||||
const inAppBrowserPref = useInAppBrowser()
|
||||
const setUseInAppBrowser = useSetInAppBrowser()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Content and Media`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/saved-feeds"
|
||||
label={_(msg`Manage saved feeds`)}>
|
||||
<SettingsList.ItemIcon icon={HashtagIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Manage saved feeds</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/threads"
|
||||
label={_(msg`Thread preferences`)}>
|
||||
<SettingsList.ItemIcon icon={BubblesIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Thread preferences</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/following-feed"
|
||||
label={_(msg`Following feed preferences`)}>
|
||||
<SettingsList.ItemIcon icon={HomeIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Following feed preferences</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/external-embeds"
|
||||
label={_(msg`External media`)}>
|
||||
<SettingsList.ItemIcon icon={MacintoshIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>External media</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
name="use_in_app_browser"
|
||||
label={_(msg`Use in-app browser to open links`)}
|
||||
value={inAppBrowserPref ?? false}
|
||||
onChange={value => setUseInAppBrowser(value)}>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={WindowIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Use in-app browser to open links</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Platform />
|
||||
</SettingsList.Item>
|
||||
</Toggle.Item>
|
||||
)}
|
||||
<Toggle.Item
|
||||
name="disable_autoplay"
|
||||
label={_(msg`Disable autoplay for videos and GIFs`)}
|
||||
value={autoplayDisabledPref}
|
||||
onChange={value => setAutoplayDisabledPref(value)}>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={PlayIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Disable autoplay for videos and GIFs</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Toggle.Platform />
|
||||
</SettingsList.Item>
|
||||
</Toggle.Item>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useAppPasswordsQuery} from '#/state/queries/app-passwords'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
|
||||
import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
|
||||
import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import {Email2FAToggle} from './components/Email2FAToggle'
|
||||
import {PwiOptOut} from './components/PwiOptOut'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'PrivacyAndSecuritySettings'
|
||||
>
|
||||
export function PrivacyAndSecuritySettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {data: appPasswords} = useAppPasswordsQuery()
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Privacy and Security`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<SettingsList.Item>
|
||||
<SettingsList.ItemIcon icon={VerifiedIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Two-factor authentication (2FA)</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<Email2FAToggle />
|
||||
</SettingsList.Item>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/app-passwords"
|
||||
label={_(msg`App passwords`)}>
|
||||
<SettingsList.ItemIcon icon={KeyIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>App passwords</Trans>
|
||||
</SettingsList.ItemText>
|
||||
{appPasswords && appPasswords.length > 0 && (
|
||||
<SettingsList.BadgeText>
|
||||
{appPasswords.length}
|
||||
</SettingsList.BadgeText>
|
||||
)}
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.Group>
|
||||
<SettingsList.ItemIcon icon={EyeSlashIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Logged-out visibility</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<PwiOptOut />
|
||||
</SettingsList.Group>
|
||||
<SettingsList.Item>
|
||||
<Admonition.Outer type="tip" style={[a.flex_1]}>
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<View style={[a.flex_1, a.gap_sm]}>
|
||||
<Admonition.Text>
|
||||
<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>
|
||||
</Admonition.Text>
|
||||
<Admonition.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>
|
||||
</Admonition.Text>
|
||||
</View>
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
</SettingsList.Item>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Linking} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
import {HELP_DESK_URL} from '#/lib/constants'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSession, useSessionApi} from '#/state/session'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {ProfileHeaderDisplayName} from '#/screens/Profile/Header/DisplayName'
|
||||
import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle'
|
||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||
import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo'
|
||||
import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
|
||||
import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
|
||||
import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
|
||||
import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller'
|
||||
import {
|
||||
Person_Stroke2_Corner2_Rounded as PersonIcon,
|
||||
PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon,
|
||||
} from '#/components/icons/Person'
|
||||
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
|
||||
import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||
export function SettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const {logoutEveryAccount} = useSessionApi()
|
||||
const {accounts, currentAccount} = useSession()
|
||||
const switchAccountControl = useDialogControl()
|
||||
const signOutPromptControl = Prompt.usePromptControl()
|
||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeEverything = useCloseAllActiveElements()
|
||||
|
||||
const onAddAnotherAccount = () => {
|
||||
setShowLoggedOut(true)
|
||||
closeEverything()
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header title={_(msg`Settings`)} />
|
||||
<Layout.Content>
|
||||
<SettingsList.Container>
|
||||
<View
|
||||
style={[
|
||||
a.px_xl,
|
||||
a.pb_md,
|
||||
a.w_full,
|
||||
a.gap_2xs,
|
||||
a.align_center,
|
||||
{minHeight: 160},
|
||||
]}>
|
||||
{profile && <ProfilePreview profile={profile} />}
|
||||
</View>
|
||||
<SettingsList.PressableItem
|
||||
label={
|
||||
accounts.length > 1
|
||||
? _(msg`Switch account`)
|
||||
: _(msg`Add another account`)
|
||||
}
|
||||
onPress={() =>
|
||||
accounts.length > 1
|
||||
? switchAccountControl.open()
|
||||
: onAddAnotherAccount()
|
||||
}>
|
||||
<SettingsList.ItemIcon icon={PersonGroupIcon} />
|
||||
<SettingsList.ItemText>
|
||||
{accounts.length > 1 ? (
|
||||
<Trans>Switch account</Trans>
|
||||
) : (
|
||||
<Trans>Add another account</Trans>
|
||||
)}
|
||||
</SettingsList.ItemText>
|
||||
{accounts.length > 1 && (
|
||||
<AvatarStack
|
||||
profiles={accounts
|
||||
.map(acc => acc.did)
|
||||
.filter(did => did !== currentAccount?.did)
|
||||
.slice(0, 5)}
|
||||
/>
|
||||
)}
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}>
|
||||
<SettingsList.ItemIcon icon={PersonIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Account</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/privacy-and-security"
|
||||
label={_(msg`Privacy and security`)}>
|
||||
<SettingsList.ItemIcon icon={LockIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Privacy and security</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}>
|
||||
<SettingsList.ItemIcon icon={HandIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Moderation</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/content-and-media"
|
||||
label={_(msg`Content and media`)}>
|
||||
<SettingsList.ItemIcon icon={WindowIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Content and media</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/appearance"
|
||||
label={_(msg`Appearance`)}>
|
||||
<SettingsList.ItemIcon icon={PaintRollerIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Appearance</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/accessibility"
|
||||
label={_(msg`Accessibility`)}>
|
||||
<SettingsList.ItemIcon icon={AccessibilityIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Accessibility</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/language"
|
||||
label={_(msg`Languages`)}>
|
||||
<SettingsList.ItemIcon icon={EarthIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Languages</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.PressableItem
|
||||
onPress={() => Linking.openURL(HELP_DESK_URL)}
|
||||
label={_(msg`Help`)}
|
||||
accessibilityHint={_(msg`Open helpdesk in browser`)}>
|
||||
<SettingsList.ItemIcon icon={CircleQuestionIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Help</Trans>
|
||||
</SettingsList.ItemText>
|
||||
<SettingsList.Chevron />
|
||||
</SettingsList.PressableItem>
|
||||
<SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
|
||||
<SettingsList.ItemIcon icon={BubbleInfoIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>About</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
<SettingsList.PressableItem
|
||||
destructive
|
||||
onPress={() => signOutPromptControl.open()}
|
||||
label={_(msg`Sign out`)}>
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Sign out</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.PressableItem>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
<Prompt.Basic
|
||||
control={signOutPromptControl}
|
||||
title={_(msg`Sign out?`)}
|
||||
description={_(msg`You will be signed out of all your accounts.`)}
|
||||
onConfirm={() => logoutEveryAccount('Settings')}
|
||||
confirmButtonCta={_(msg`Sign out`)}
|
||||
cancelButtonCta={_(msg`Cancel`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
|
||||
<SwitchAccountDialog control={switchAccountControl} />
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function ProfilePreview({
|
||||
profile,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
}) {
|
||||
const shadow = useProfileShadow(profile)
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
if (!moderationOpts) return null
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
avatar={shadow.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
<ProfileHeaderDisplayName profile={shadow} moderation={moderation} />
|
||||
<ProfileHeaderHandle profile={shadow} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const AVI_SIZE = 26
|
||||
const HALF_AVI_SIZE = AVI_SIZE / 2
|
||||
|
||||
function AvatarStack({profiles}: {profiles: string[]}) {
|
||||
const {data, error} = useProfilesQuery({handles: profiles})
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return null
|
||||
}
|
||||
|
||||
const isPending = !data || !moderationOpts
|
||||
|
||||
const items = isPending
|
||||
? Array.from({length: profiles.length}).map((_, i) => ({
|
||||
key: i,
|
||||
profile: null,
|
||||
moderation: null,
|
||||
}))
|
||||
: data.profiles.map(item => ({
|
||||
key: item.did,
|
||||
profile: item,
|
||||
moderation: moderateProfile(item, moderationOpts),
|
||||
}))
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.relative,
|
||||
{width: AVI_SIZE + (items.length - 1) * HALF_AVI_SIZE},
|
||||
]}>
|
||||
{items.map((item, i) => (
|
||||
<View
|
||||
key={item.key}
|
||||
style={[
|
||||
t.atoms.bg_contrast_25,
|
||||
a.relative,
|
||||
{
|
||||
width: AVI_SIZE,
|
||||
height: AVI_SIZE,
|
||||
left: i * -HALF_AVI_SIZE,
|
||||
borderWidth: 1,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
borderRadius: 999,
|
||||
zIndex: 3 - i,
|
||||
},
|
||||
]}>
|
||||
{item.profile && (
|
||||
<UserAvatar
|
||||
size={AVI_SIZE - 2}
|
||||
avatar={item.profile.avatar}
|
||||
moderation={item.moderation.ui('avatar')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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 {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import * as SettingsList from './SettingsList'
|
||||
|
||||
export function Email2FAToggle() {
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const {openModal} = useModalControls()
|
||||
const disableDialogControl = useDialogControl()
|
||||
const enableDialogControl = 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) {
|
||||
disableDialogControl.open()
|
||||
} else {
|
||||
if (!currentAccount.emailConfirmed) {
|
||||
openModal({
|
||||
name: 'verify-email',
|
||||
onSuccess: enableDialogControl.open,
|
||||
})
|
||||
return
|
||||
}
|
||||
enableDialogControl.open()
|
||||
}
|
||||
}, [currentAccount, enableDialogControl, openModal, disableDialogControl])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DisableEmail2FADialog control={disableDialogControl} />
|
||||
<Prompt.Basic
|
||||
control={enableDialogControl}
|
||||
title={_(msg`Enable Email 2FA`)}
|
||||
description={_(msg`Require an email code to log in to your account.`)}
|
||||
onConfirm={enableEmailAuthFactor}
|
||||
confirmButtonCta={_(msg`Enable`)}
|
||||
/>
|
||||
<SettingsList.BadgeButton
|
||||
label={
|
||||
currentAccount?.emailAuthFactor ? _(msg`Disable`) : _(msg`Enable`)
|
||||
}
|
||||
onPress={onToggle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {
|
||||
useProfileQuery,
|
||||
useProfileUpdateMutation,
|
||||
} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export 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.flex_1, a.gap_sm]}>
|
||||
<Toggle.Item
|
||||
name="logged_out_visibility"
|
||||
disabled={!canToggle || updateProfile.isPending}
|
||||
value={isOptedOut}
|
||||
onChange={onToggleOptOut}
|
||||
label={_(
|
||||
msg`Discourage apps from showing my account to logged-out users`,
|
||||
)}
|
||||
style={[a.w_full]}>
|
||||
<Toggle.LabelText style={[a.flex_1]}>
|
||||
<Trans>
|
||||
Discourage apps from showing my account to logged-out users
|
||||
</Trans>
|
||||
</Toggle.LabelText>
|
||||
<Toggle.Platform />
|
||||
</Toggle.Item>
|
||||
|
||||
<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>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import React, {useContext, useMemo} from 'react'
|
||||
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as Button from '#/components/Button'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||
import {Link, LinkProps} from '#/components/Link'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const ItemContext = React.createContext({
|
||||
destructive: false,
|
||||
withinGroup: false,
|
||||
})
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
export function Container({children}: {children: React.ReactNode}) {
|
||||
return <View style={[a.flex_1, a.py_lg]}>{children}</View>
|
||||
}
|
||||
|
||||
/**
|
||||
* This uses `Portal` magic ✨ to render the icons and title correctly. ItemIcon and ItemText components
|
||||
* get teleported to the top row, leaving the rest of the children in the bottom row.
|
||||
*/
|
||||
export function Group({
|
||||
children,
|
||||
destructive = false,
|
||||
iconInset = true,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
destructive?: boolean
|
||||
iconInset?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const context = useMemo(
|
||||
() => ({destructive, withinGroup: true}),
|
||||
[destructive],
|
||||
)
|
||||
return (
|
||||
<View style={[a.w_full, style]}>
|
||||
<Portal.Provider>
|
||||
<ItemContext.Provider value={context}>
|
||||
<Item style={[a.pb_2xs, {minHeight: 42}]}>
|
||||
<Portal.Outlet />
|
||||
</Item>
|
||||
<Item
|
||||
style={[
|
||||
a.flex_col,
|
||||
a.pt_2xs,
|
||||
a.align_start,
|
||||
a.gap_0,
|
||||
contentContainerStyle,
|
||||
]}
|
||||
iconInset={iconInset}>
|
||||
{children}
|
||||
</Item>
|
||||
</ItemContext.Provider>
|
||||
</Portal.Provider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Item({
|
||||
children,
|
||||
destructive,
|
||||
iconInset = false,
|
||||
style,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
destructive?: boolean
|
||||
/**
|
||||
* Adds left padding so that the content will be aligned with other Items that contain icons
|
||||
* @default false
|
||||
*/
|
||||
iconInset?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const context = useContext(ItemContext)
|
||||
const childContext = useMemo(() => {
|
||||
if (typeof destructive !== 'boolean') return context
|
||||
return {...context, destructive}
|
||||
}, [context, destructive])
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.px_xl,
|
||||
a.py_sm,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
a.w_full,
|
||||
a.flex_row,
|
||||
{minHeight: 48},
|
||||
iconInset && {
|
||||
paddingLeft:
|
||||
// existing padding
|
||||
a.pl_xl.paddingLeft +
|
||||
// icon
|
||||
28 +
|
||||
// gap
|
||||
a.gap_md.gap,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
<ItemContext.Provider value={childContext}>
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function LinkItem({
|
||||
children,
|
||||
destructive = false,
|
||||
contentContainerStyle,
|
||||
chevronColor,
|
||||
...props
|
||||
}: LinkProps & {
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
destructive?: boolean
|
||||
chevronColor?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<Link color="secondary" {...props}>
|
||||
{args => (
|
||||
<Item
|
||||
destructive={destructive}
|
||||
style={[
|
||||
(args.hovered || args.pressed) && [t.atoms.bg_contrast_25],
|
||||
contentContainerStyle,
|
||||
]}>
|
||||
{typeof children === 'function' ? children(args) : children}
|
||||
<Chevron color={chevronColor} />
|
||||
</Item>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function PressableItem({
|
||||
children,
|
||||
destructive = false,
|
||||
contentContainerStyle,
|
||||
hoverStyle,
|
||||
...props
|
||||
}: Button.ButtonProps & {
|
||||
contentContainerStyle?: StyleProp<ViewStyle>
|
||||
destructive?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Button.Button {...props}>
|
||||
{args => (
|
||||
<Item
|
||||
destructive={destructive}
|
||||
style={[
|
||||
(args.hovered || args.pressed) && [
|
||||
t.atoms.bg_contrast_25,
|
||||
hoverStyle,
|
||||
],
|
||||
contentContainerStyle,
|
||||
]}>
|
||||
{typeof children === 'function' ? children(args) : children}
|
||||
</Item>
|
||||
)}
|
||||
</Button.Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemIcon({
|
||||
icon: Comp,
|
||||
size = 'xl',
|
||||
color: colorProp,
|
||||
}: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & {
|
||||
color?: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {destructive, withinGroup} = useContext(ItemContext)
|
||||
|
||||
/*
|
||||
* Copied here from icons/common.tsx so we can tweak if we need to, but
|
||||
* also so that we can calculate transforms.
|
||||
*/
|
||||
const iconSize = {
|
||||
xs: 12,
|
||||
sm: 16,
|
||||
md: 20,
|
||||
lg: 24,
|
||||
xl: 28,
|
||||
'2xl': 32,
|
||||
}[size]
|
||||
|
||||
const color =
|
||||
colorProp ?? (destructive ? t.palette.negative_500 : t.atoms.text.color)
|
||||
|
||||
const content = (
|
||||
<View style={[a.z_20, {width: iconSize, height: iconSize}]}>
|
||||
<Comp width={iconSize} style={[{color}]} />
|
||||
</View>
|
||||
)
|
||||
|
||||
if (withinGroup) {
|
||||
return <Portal.Portal>{content}</Portal.Portal>
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export function ItemText({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button.ButtonText>) {
|
||||
const t = useTheme()
|
||||
const {destructive, withinGroup} = useContext(ItemContext)
|
||||
|
||||
const content = (
|
||||
<Button.ButtonText
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_normal,
|
||||
a.text_left,
|
||||
a.flex_1,
|
||||
destructive ? {color: t.palette.negative_500} : t.atoms.text,
|
||||
style,
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (withinGroup) {
|
||||
return <Portal.Portal>{content}</Portal.Portal>
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
}
|
||||
|
||||
export function Divider() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[a.border_t, t.atoms.border_contrast_medium, a.w_full, a.my_sm]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Chevron({color: colorProp}: {color?: string}) {
|
||||
const {destructive} = useContext(ItemContext)
|
||||
const t = useTheme()
|
||||
const color =
|
||||
colorProp ?? (destructive ? t.palette.negative_500 : t.palette.contrast_500)
|
||||
return <ItemIcon icon={ChevronRightIcon} size="md" color={color} />
|
||||
}
|
||||
|
||||
export function BadgeText({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_low,
|
||||
a.text_md,
|
||||
a.text_right,
|
||||
a.leading_snug,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function BadgeButton({
|
||||
label,
|
||||
onPress,
|
||||
}: {
|
||||
label: string
|
||||
onPress: (evt: GestureResponderEvent) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<Button.Button label={label} onPress={onPress} hitSlop={HITSLOP_10}>
|
||||
{({pressed}) => (
|
||||
<Button.ButtonText
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_normal,
|
||||
a.text_right,
|
||||
{color: pressed ? t.palette.contrast_300 : t.palette.primary_500},
|
||||
]}>
|
||||
{label}
|
||||
</Button.ButtonText>
|
||||
)}
|
||||
</Button.Button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user