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:
Samuel Newman
2024-10-29 21:14:54 +00:00
committed by GitHub
parent ab492cd77a
commit c8f264b78b
59 changed files with 1811 additions and 133 deletions
+16 -11
View File
@@ -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>
+78
View File
@@ -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>
)
}
+180
View File
@@ -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>
)
}
+76 -89
View File
@@ -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>
)
}
+282
View File
@@ -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>
)
}