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

* move export car dialog

* move disableemail2fadialog

* delete old settings screens

* fix type error

* Update Navigation.tsx

* Delete AccountDropdownBtn.tsx

* remove old change handle modal

* delete add app paswords

* forgot to actually delete the change handle modal
This commit is contained in:
Samuel Newman
2024-11-08 22:42:18 +00:00
committed by GitHub
parent e8a03058bb
commit 5da3f29498
22 changed files with 16 additions and 3768 deletions
-307
View File
@@ -1,307 +0,0 @@
import React, {useState} from 'react'
import {StyleSheet, TextInput, TouchableOpacity, View} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {
useAppPasswordCreateMutation,
useAppPasswordsQuery,
} from '#/state/queries/app-passwords'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
export const snapPoints = ['90%']
const shadesOfBlue: string[] = [
'AliceBlue',
'Aqua',
'Aquamarine',
'Azure',
'BabyBlue',
'Blue',
'BlueViolet',
'CadetBlue',
'CornflowerBlue',
'Cyan',
'DarkBlue',
'DarkCyan',
'DarkSlateBlue',
'DeepSkyBlue',
'DodgerBlue',
'ElectricBlue',
'LightBlue',
'LightCyan',
'LightSkyBlue',
'LightSteelBlue',
'MediumAquaMarine',
'MediumBlue',
'MediumSlateBlue',
'MidnightBlue',
'Navy',
'PowderBlue',
'RoyalBlue',
'SkyBlue',
'SlateBlue',
'SteelBlue',
'Teal',
'Turquoise',
]
export function Component({}: {}) {
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
const {data: passwords} = useAppPasswordsQuery()
const {mutateAsync: mutateAppPassword, isPending} =
useAppPasswordCreateMutation()
const [name, setName] = useState(
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
)
const [appPassword, setAppPassword] = useState<string>()
const [wasCopied, setWasCopied] = useState(false)
const [privileged, setPrivileged] = useState(false)
const onCopy = React.useCallback(() => {
if (appPassword) {
setStringAsync(appPassword)
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
setWasCopied(true)
}
}, [appPassword, _])
const onDone = React.useCallback(() => {
closeModal()
}, [closeModal])
const createAppPassword = async () => {
// if name is all whitespace, we don't allow it
if (!name || !name.trim()) {
Toast.show(
_(
msg`Please enter a name for your app password. All spaces is not allowed.`,
),
'xmark',
)
return
}
// if name is too short (under 4 chars), we don't allow it
if (name.length < 4) {
Toast.show(
_(msg`App Password names must be at least 4 characters long.`),
'xmark',
)
return
}
if (passwords?.find(p => p.name === name)) {
Toast.show(_(msg`This name is already in use`), 'xmark')
return
}
try {
const newPassword = await mutateAppPassword({name, privileged})
if (newPassword) {
setAppPassword(newPassword.password)
} else {
Toast.show(_(msg`Failed to create app password.`), 'xmark')
// TODO: better error handling (?)
}
} catch (e) {
Toast.show(_(msg`Failed to create app password.`), 'xmark')
logger.error('Failed to create app password', {message: e})
}
}
const _onChangeText = (text: string) => {
// sanitize input
// we only all alphanumeric characters, spaces, dashes, and underscores
// if the user enters anything else, we ignore it and shake the input container
// also, it cannot start with a space
if (text.match(/^[a-zA-Z0-9-_ ]*$/)) {
setName(text)
} else {
Toast.show(
_(
msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
),
'xmark',
)
}
}
return (
<View style={[styles.container, pal.view]} testID="addAppPasswordsModal">
{!appPassword ? (
<>
<View>
<Text type="lg" style={[pal.text]}>
<Trans>
Please enter a unique name for this App Password or use our
randomly generated one.
</Trans>
</Text>
<View style={[pal.btn, styles.textInputWrapper]}>
<TextInput
style={[styles.input, pal.text]}
onChangeText={_onChangeText}
value={name}
placeholder={_(msg`Enter a name for this App Password`)}
placeholderTextColor={pal.colors.textLight}
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
autoFocus={true}
maxLength={32}
selectTextOnFocus={true}
blurOnSubmit={true}
editable={!isPending}
returnKeyType="done"
onSubmitEditing={createAppPassword}
accessible={true}
accessibilityLabel={_(msg`Name`)}
accessibilityHint={_(msg`Input name for app password`)}
/>
</View>
</View>
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
<Trans>
Can only contain letters, numbers, spaces, dashes, and
underscores. Must be at least 4 characters long, but no more than
32 characters long.
</Trans>
</Text>
<Toggle.Item
type="checkbox"
label={_(msg`Allow access to your direct messages`)}
value={privileged}
onChange={val => setPrivileged(val)}
name="privileged"
style={a.my_md}>
<Toggle.Checkbox />
<Toggle.LabelText>
<Trans>Allow access to your direct messages</Trans>
</Toggle.LabelText>
</Toggle.Item>
</>
) : (
<>
<View>
<Text type="lg" style={[pal.text]}>
<Text type="lg-bold" style={[pal.text, s.mr5]}>
<Trans>Here is your app password.</Trans>
</Text>
<Trans>
Use this to sign into the other app along with your handle.
</Trans>
</Text>
<TouchableOpacity
style={[pal.border, styles.passwordContainer, pal.btn]}
onPress={onCopy}
accessibilityRole="button"
accessibilityLabel={_(msg`Copy`)}
accessibilityHint={_(msg`Copies app password`)}>
<Text type="2xl-bold" style={[pal.text]}>
{appPassword}
</Text>
{wasCopied ? (
<Text style={[pal.textLight]}>
<Trans>Copied</Trans>
</Text>
) : (
<FontAwesomeIcon
icon={['far', 'clone']}
style={pal.text as FontAwesomeIconStyle}
size={18}
/>
)}
</TouchableOpacity>
</View>
<Text type="lg" style={[pal.textLight, s.mb10]}>
<Trans>
For security reasons, you won't be able to view this again. If you
lose this password, you'll need to generate a new one.
</Trans>
</Text>
</>
)}
<View style={styles.btnContainer}>
<Button
type="primary"
label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={!appPassword ? createAppPassword : onDone}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isNative ? 50 : 0,
paddingHorizontal: 16,
},
textInputWrapper: {
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
marginTop: 16,
marginBottom: 8,
},
input: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 8,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
passwordContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: 'center',
borderRadius: 10,
marginTop: 16,
marginBottom: 12,
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 12,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
paddingHorizontal: 60,
paddingVertical: 14,
},
btnLabel: {
fontSize: 18,
},
groupContent: {
borderTopWidth: 1,
flexDirection: 'row',
alignItems: 'center',
},
})
-614
View File
@@ -1,614 +0,0 @@
import React, {useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '#/lib/strings/handles'
import {s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
export const snapPoints = ['100%']
export type Props = {onChanged: () => void}
export function Component(props: Props) {
const {currentAccount} = useSession()
const agent = useAgent()
const {
isLoading,
data: serviceInfo,
error: serviceInfoError,
} = useServiceQuery(agent.service.toString())
return isLoading || !currentAccount ? (
<View style={{padding: 18}}>
<ActivityIndicator />
</View>
) : serviceInfoError || !serviceInfo ? (
<ErrorMessage message={cleanError(serviceInfoError)} />
) : (
<Inner
{...props}
currentAccount={currentAccount}
serviceInfo={serviceInfo}
/>
)
}
export function Inner({
currentAccount,
serviceInfo,
onChanged,
}: Props & {
currentAccount: SessionAccount
serviceInfo: ComAtprotoServerDescribeServer.OutputSchema
}) {
const {_} = useLingui()
const pal = usePalette('default')
const {closeModal} = useModalControls()
const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} =
useUpdateHandleMutation()
const agent = useAgent()
const [error, setError] = useState<string>('')
const [isCustom, setCustom] = React.useState<boolean>(false)
const [handle, setHandle] = React.useState<string>('')
const [canSave, setCanSave] = React.useState<boolean>(false)
const userDomain = serviceInfo.availableUserDomains?.[0]
// events
// =
const onPressCancel = React.useCallback(() => {
closeModal()
}, [closeModal])
const onToggleCustom = React.useCallback(() => {
// toggle between a provided domain vs a custom one
setHandle('')
setCanSave(false)
setCustom(!isCustom)
}, [setCustom, isCustom])
const onPressSave = React.useCallback(async () => {
if (!userDomain) {
logger.error(`ChangeHandle: userDomain is undefined`, {
service: serviceInfo,
})
setError(`The service you've selected has no domains configured.`)
return
}
try {
const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
logger.debug(`Updating handle to ${newHandle}`)
await updateHandle({
handle: newHandle,
})
await agent.resumeSession(agent.session!)
closeModal()
onChanged()
} catch (err: any) {
setError(cleanError(err))
logger.error('Failed to update handle', {handle, message: err})
} finally {
}
}, [
setError,
handle,
userDomain,
isCustom,
onChanged,
closeModal,
updateHandle,
serviceInfo,
agent,
])
// rendering
// =
return (
<View style={[s.flex1, pal.view]}>
<View style={[styles.title, pal.border]}>
<View style={styles.titleLeft}>
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel change handle`)}
accessibilityHint={_(msg`Exits handle change process`)}
onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</View>
<Text
type="2xl-bold"
style={[styles.titleMiddle, pal.text]}
numberOfLines={1}>
<Trans>Change Handle</Trans>
</Text>
<View style={styles.titleRight}>
{isUpdateHandlePending ? (
<ActivityIndicator />
) : canSave ? (
<TouchableOpacity
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel={_(msg`Save handle change`)}
accessibilityHint={_(msg`Saves handle change to ${handle}`)}>
<Text type="2xl-medium" style={pal.link}>
<Trans>Save</Trans>
</Text>
</TouchableOpacity>
) : undefined}
</View>
</View>
<ScrollView style={styles.inner}>
{error !== '' && (
<View style={styles.errorContainer}>
<ErrorMessage message={error} />
</View>
)}
{isCustom ? (
<CustomHandleForm
currentAccount={currentAccount}
handle={handle}
isProcessing={isUpdateHandlePending}
canSave={canSave}
onToggleCustom={onToggleCustom}
setHandle={setHandle}
setCanSave={setCanSave}
onPressSave={onPressSave}
/>
) : (
<ProvidedHandleForm
handle={handle}
userDomain={userDomain}
isProcessing={isUpdateHandlePending}
onToggleCustom={onToggleCustom}
setHandle={setHandle}
setCanSave={setCanSave}
/>
)}
</ScrollView>
</View>
)
}
/**
* The form for using a domain allocated by the PDS
*/
function ProvidedHandleForm({
userDomain,
handle,
isProcessing,
setHandle,
onToggleCustom,
setCanSave,
}: {
userDomain: string
handle: string
isProcessing: boolean
setHandle: (v: string) => void
onToggleCustom: () => void
setCanSave: (v: boolean) => void
}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
// events
// =
const onChangeHandle = React.useCallback(
(v: string) => {
const newHandle = makeValidHandle(v)
setHandle(newHandle)
setCanSave(newHandle.length > 0)
},
[setHandle, setCanSave],
)
// rendering
// =
return (
<>
<View style={[pal.btn, styles.textInputWrapper]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.textInputIcon]}
/>
<TextInput
testID="setHandleInput"
style={[pal.text, styles.textInput]}
placeholder={_(msg`e.g. alice`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
keyboardAppearance={theme.colorScheme}
value={handle}
onChangeText={onChangeHandle}
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Handle`)}
accessibilityHint={_(msg`Sets Bluesky username`)}
/>
</View>
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
<Trans>
Your full handle will be{' '}
<Text type="md-bold" style={pal.textLight}>
@{createFullHandle(handle, userDomain)}
</Text>
</Trans>
</Text>
<TouchableOpacity
onPress={onToggleCustom}
accessibilityRole="button"
accessibilityLabel={_(msg`Hosting provider`)}
accessibilityHint={_(msg`Opens modal for using custom domain`)}>
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
<Trans>I have my own domain</Trans>
</Text>
</TouchableOpacity>
</>
)
}
/**
* The form for using a custom domain
*/
function CustomHandleForm({
currentAccount,
handle,
canSave,
isProcessing,
setHandle,
onToggleCustom,
onPressSave,
setCanSave,
}: {
currentAccount: SessionAccount
handle: string
canSave: boolean
isProcessing: boolean
setHandle: (v: string) => void
onToggleCustom: () => void
onPressSave: () => void
setCanSave: (v: boolean) => void
}) {
const pal = usePalette('default')
const palSecondary = usePalette('secondary')
const palError = usePalette('error')
const theme = useTheme()
const {_} = useLingui()
const [isVerifying, setIsVerifying] = React.useState(false)
const [error, setError] = React.useState<string>('')
const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
const fetchDid = useFetchDid()
// events
// =
const onPressCopy = React.useCallback(() => {
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
(v: string) => {
setHandle(v)
setCanSave(false)
},
[setHandle, setCanSave],
)
const onPressVerify = React.useCallback(async () => {
if (canSave) {
onPressSave()
}
try {
setIsVerifying(true)
setError('')
const did = await fetchDid(handle)
if (did === currentAccount.did) {
setCanSave(true)
} else {
setError(`Incorrect DID returned (got ${did})`)
}
} catch (err: any) {
setError(cleanError(err))
logger.error('Failed to verify domain', {handle, error: err})
} finally {
setIsVerifying(false)
}
}, [
handle,
currentAccount,
setIsVerifying,
setCanSave,
setError,
canSave,
onPressSave,
fetchDid,
])
// rendering
// =
return (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
<Trans>Enter the domain you want to use</Trans>
</Text>
<View style={[pal.btn, styles.textInputWrapper]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.textInputIcon]}
/>
<TextInput
testID="setHandleInput"
style={[pal.text, styles.textInput]}
placeholder={_(msg`e.g. alice.com`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
keyboardAppearance={theme.colorScheme}
value={handle}
onChangeText={onChangeHandle}
editable={!isProcessing}
accessibilityLabelledBy="customDomain"
accessibilityLabel={_(msg`Custom domain`)}
accessibilityHint={_(msg`Input your preferred hosting provider`)}
/>
</View>
<View style={styles.spacer} />
<View style={[styles.selectableBtns]}>
<SelectableBtn
selected={isDNSForm}
label={_(msg`DNS Panel`)}
left
onSelect={() => setDNSForm(true)}
accessibilityHint={_(msg`Use the DNS panel`)}
style={s.flex1}
/>
<SelectableBtn
selected={!isDNSForm}
label={_(msg`No DNS Panel`)}
right
onSelect={() => setDNSForm(false)}
accessibilityHint={_(msg`Use a file on your server`)}
style={s.flex1}
/>
</View>
<View style={styles.spacer} />
{isDNSForm ? (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
<Trans>Add the following DNS record to your domain:</Trans>
</Text>
<View style={[styles.dnsTable, pal.btn]}>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
<Trans>Host:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
_atproto
</Text>
</View>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
<Trans>Type:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
TXT
</Text>
</View>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
<Trans>Value:</Trans>
</Text>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
did={currentAccount.did}
</Text>
</View>
</View>
<Text type="md" style={[pal.text, s.pt20, s.pl5]}>
<Trans>This should create a domain record at:</Trans>
</Text>
<Text type="mono" style={[styles.monoText, pal.text, s.pt5, s.pl5]}>
_atproto.{handle}
</Text>
</>
) : (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
<Trans>Upload a text file to:</Trans>
</Text>
<View style={[styles.valueContainer, pal.btn]}>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
https://{handle}/.well-known/atproto-did
</Text>
</View>
</View>
<View style={styles.spacer} />
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
<Trans>That contains the following:</Trans>
</Text>
<View style={[styles.valueContainer, pal.btn]}>
<View style={[styles.dnsValue]}>
<Text type="mono" style={[styles.monoText, pal.text]}>
{currentAccount.did}
</Text>
</View>
</View>
</>
)}
<View style={styles.spacer} />
<Button type="default" style={[s.p20, s.mb10]} onPress={onPressCopy}>
<Text type="xl" style={[pal.link, s.textCenter]}>
<Trans>
Copy {isDNSForm ? _(msg`Domain Value`) : _(msg`File Contents`)}
</Trans>
</Text>
</Button>
{canSave === true && (
<View style={[styles.message, palSecondary.view]}>
<Text type="md-medium" style={palSecondary.text}>
<Trans>Domain verified!</Trans>
</Text>
</View>
)}
{error ? (
<View style={[styles.message, palError.view]}>
<Text type="md-medium" style={palError.text}>
{error}
</Text>
</View>
) : null}
<Button
type="primary"
style={[s.p20, isVerifying && styles.dimmed]}
onPress={onPressVerify}>
{isVerifying ? (
<ActivityIndicator color="white" />
) : (
<Text type="xl-medium" style={[s.white, s.textCenter]}>
{canSave
? _(msg`Update to ${handle}`)
: isDNSForm
? _(msg`Verify DNS Record`)
: _(msg`Verify Text File`)}
</Text>
)}
</Button>
<View style={styles.spacer} />
<TouchableOpacity
onPress={onToggleCustom}
accessibilityLabel={_(msg`Use default provider`)}
accessibilityHint={_(msg`Use bsky.social as hosting provider`)}>
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
<Trans>Nevermind, create a handle for me</Trans>
</Text>
</TouchableOpacity>
</>
)
}
const styles = StyleSheet.create({
inner: {
padding: 14,
},
footer: {
padding: 14,
},
spacer: {
height: 20,
},
dimmed: {
opacity: 0.7,
},
selectableBtns: {
flexDirection: 'row',
},
title: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 25,
paddingHorizontal: 20,
paddingBottom: 15,
borderBottomWidth: 1,
},
titleLeft: {
width: 80,
},
titleRight: {
width: 80,
flexDirection: 'row',
justifyContent: 'flex-end',
},
titleMiddle: {
flex: 1,
textAlign: 'center',
fontSize: 21,
},
textInputWrapper: {
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
},
textInputIcon: {
marginLeft: 12,
},
textInput: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 8,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
valueContainer: {
borderRadius: 4,
paddingVertical: 16,
},
dnsTable: {
borderRadius: 4,
paddingTop: 2,
paddingBottom: 16,
},
dnsLabel: {
paddingHorizontal: 14,
paddingTop: 10,
},
dnsValue: {
paddingHorizontal: 14,
borderRadius: 4,
},
monoText: {
fontSize: 18,
lineHeight: 20,
},
message: {
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 8,
marginBottom: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 10,
marginBottom: 10,
},
errorContainer: {marginBottom: 10},
})
-8
View File
@@ -7,9 +7,7 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useModalControls, useModals} from '#/state/modals'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangeHandleModal from './ChangeHandle'
import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as DeleteAccountModal from './DeleteAccount'
@@ -69,15 +67,9 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'delete-account') {
snapPoints = DeleteAccountModal.snapPoints
element = <DeleteAccountModal.Component />
} else if (activeModal?.name === 'change-handle') {
snapPoints = ChangeHandleModal.snapPoints
element = <ChangeHandleModal.Component {...activeModal} />
} else if (activeModal?.name === 'invite-codes') {
snapPoints = InviteCodesModal.snapPoints
element = <InviteCodesModal.Component />
} else if (activeModal?.name === 'add-app-password') {
snapPoints = AddAppPassword.snapPoints
element = <AddAppPassword.Component />
} else if (activeModal?.name === 'content-languages-settings') {
snapPoints = ContentLanguagesSettingsModal.snapPoints
element = <ContentLanguagesSettingsModal.Component />
-6
View File
@@ -7,9 +7,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import type {Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
import * as AddAppPassword from './AddAppPasswords'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangeHandleModal from './ChangeHandle'
import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as CropImageModal from './CropImage.web'
@@ -74,12 +72,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <CropImageModal.Component {...modal} />
} else if (modal.name === 'delete-account') {
element = <DeleteAccountModal.Component />
} else if (modal.name === 'change-handle') {
element = <ChangeHandleModal.Component {...modal} />
} else if (modal.name === 'invite-codes') {
element = <InviteCodesModal.Component />
} else if (modal.name === 'add-app-password') {
element = <AddAppPassword.Component />
} else if (modal.name === 'content-languages-settings') {
element = <ContentLanguagesSettingsModal.Component />
} else if (modal.name === 'post-languages-settings') {
-66
View File
@@ -1,66 +0,0 @@
import React from 'react'
import {Pressable} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {s} from '#/lib/styles'
import {SessionAccount, useSessionApi} from '#/state/session'
import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import * as Toast from '../../com/util/Toast'
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
export function AccountDropdownBtn({account}: {account: SessionAccount}) {
const pal = usePalette('default')
const {removeAccount} = useSessionApi()
const removePromptControl = useDialogControl()
const {_} = useLingui()
const items: DropdownItem[] = [
{
label: _(msg`Remove account`),
onPress: removePromptControl.open,
icon: {
ios: {
name: 'trash',
},
android: 'ic_delete',
web: ['far', 'trash-can'],
},
},
]
return (
<>
<Pressable accessibilityRole="button" style={s.pl10}>
<NativeDropdown
testID="accountSettingsDropdownBtn"
items={items}
accessibilityLabel={_(msg`Account options`)}
accessibilityHint="">
<FontAwesomeIcon
icon="ellipsis-h"
style={pal.textLight as FontAwesomeIconStyle}
/>
</NativeDropdown>
</Pressable>
<Prompt.Basic
control={removePromptControl}
title={_(msg`Remove from quick access?`)}
description={_(
msg`This will remove @${account.handle} from the quick access list.`,
)}
onConfirm={() => {
removeAccount(account)
Toast.show(_(msg`Account removed from quick access`))
}}
confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative"
/>
</>
)
}
+3 -1
View File
@@ -448,7 +448,9 @@ let Row = function RowImpl<ItemT>({
onItemSeen: ((item: any) => void) | undefined
}): React.ReactNode {
const rowRef = React.useRef(null)
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
const intersectionTimeout = React.useRef<
ReturnType<typeof setTimeout> | undefined
>(undefined)
const handleIntersection = useNonReactiveCallback(
(entries: IntersectionObserverEntry[]) => {
-157
View File
@@ -1,157 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {
useAutoplayDisabled,
useHapticsDisabled,
useRequireAltTextEnabled,
useSetAutoplayDisabled,
useSetHapticsDisabled,
useSetRequireAltTextEnabled,
} from '#/state/preferences'
import {
useLargeAltBadgeEnabled,
useSetLargeAltBadgeEnabled,
} from '#/state/preferences/large-alt-badge'
import {useSetMinimalShellMode} from '#/state/shell'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {AccessibilitySettingsScreen as NewAccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySettings'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'AccessibilitySettings'
>
export function AccessibilitySettingsScreen(props: Props) {
return IS_INTERNAL ? (
<NewAccessibilitySettingsScreen {...props} />
) : (
<LegacyAccessibilitySettingsScreen {...props} />
)
}
function LegacyAccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
const autoplayDisabled = useAutoplayDisabled()
const setAutoplayDisabled = useSetAutoplayDisabled()
const hapticsDisabled = useHapticsDisabled()
const setHapticsDisabled = useSetHapticsDisabled()
const largeAltBadgeEnabled = useLargeAltBadgeEnabled()
const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="accessibilitySettingsScreen">
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[
pal.border,
a.border_b,
!isMobile && {
borderLeftWidth: StyleSheet.hairlineWidth,
borderRightWidth: StyleSheet.hairlineWidth,
},
]}>
<View style={a.flex_1}>
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
<Trans>Accessibility Settings</Trans>
</Text>
</View>
</SimpleViewHeader>
<ScrollView
// @ts-ignore web only -prf
dataSet={{'stable-gutters': 1}}
style={s.flex1}
contentContainerStyle={[
s.flex1,
{paddingBottom: 100},
isMobile && pal.viewLight,
]}>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Alt text</Trans>
</Text>
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={_(msg`Require alt text before posting`)}
labelType="lg"
isSelected={requireAltTextEnabled}
onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)}
/>
<ToggleButton
type="default-light"
label={_(msg`Display larger alt text badges`)}
labelType="lg"
isSelected={!!largeAltBadgeEnabled}
onPress={() => setLargeAltBadgeEnabled(!largeAltBadgeEnabled)}
/>
</View>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Media</Trans>
</Text>
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={_(msg`Disable autoplay for videos and GIFs`)}
labelType="lg"
isSelected={autoplayDisabled}
onPress={() => setAutoplayDisabled(!autoplayDisabled)}
/>
</View>
{isNative && (
<>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Haptics</Trans>
</Text>
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={_(msg`Disable haptic feedback`)}
labelType="lg"
isSelected={hapticsDisabled}
onPress={() => setHapticsDisabled(!hapticsDisabled)}
/>
</View>
</>
)}
</ScrollView>
</Layout.Screen>
)
}
const styles = StyleSheet.create({
heading: {
paddingHorizontal: 18,
paddingTop: 14,
paddingBottom: 6,
},
toggleCard: {
paddingVertical: 8,
paddingHorizontal: 6,
marginBottom: 1,
},
})
-375
View File
@@ -1,375 +0,0 @@
import React from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {useModalControls} from '#/state/modals'
import {
useAppPasswordDeleteMutation,
useAppPasswordsQuery,
} from '#/state/queries/app-passwords'
import {useSetMinimalShellMode} from '#/state/shell'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import * as Layout from '#/components/Layout'
import * as Prompt from '#/components/Prompt'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export function AppPasswords(props: Props) {
return IS_INTERNAL ? (
<NewAppPasswordsScreen {...props} />
) : (
<Layout.Screen testID="AppPasswordsScreen">
<AppPasswordsInner />
</Layout.Screen>
)
}
function AppPasswordsInner() {
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
const {openModal} = useModalControls()
const {data: appPasswords, error} = useAppPasswordsQuery()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
const onAdd = React.useCallback(async () => {
openModal({name: 'add-app-password'})
}, [openModal])
if (error) {
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<ErrorScreen
title={_(msg`Oops!`)}
message={_(msg`There was an issue with fetching your app passwords`)}
details={cleanError(error)}
/>
</CenteredView>
)
}
// no app passwords (empty) state
if (appPasswords?.length === 0) {
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not created any app passwords yet. You can create one by
pressing the button below.
</Trans>
</Text>
</View>
{!isTabletOrDesktop && <View style={styles.flex1} />}
<View
style={[
styles.btnContainer,
isTabletOrDesktop && styles.btnContainerDesktop,
]}>
<Button
testID="appPasswordBtn"
type="primary"
label={_(msg`Add App Password`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
</CenteredView>
)
}
if (appPasswords?.length) {
// has app passwords
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<ScrollView
style={[
styles.scrollContainer,
pal.border,
!isTabletOrDesktop && styles.flex1,
]}>
{appPasswords.map((password, i) => (
<AppPassword
key={password.name}
testID={`appPassword-${i}`}
name={password.name}
createdAt={password.createdAt}
privileged={password.privileged}
/>
))}
{isTabletOrDesktop && (
<View style={[styles.btnContainer, styles.btnContainerDesktop]}>
<Button
testID="appPasswordBtn"
type="primary"
label={_(msg`Add App Password`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
)}
</ScrollView>
{!isTabletOrDesktop && (
<View style={styles.btnContainer}>
<Button
testID="appPasswordBtn"
type="primary"
label={_(msg`Add App Password`)}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
)}
</CenteredView>
)
}
return (
<CenteredView
style={[
styles.container,
isTabletOrDesktop && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<ActivityIndicator />
</CenteredView>
)
}
function AppPasswordsHeader() {
const {isTabletOrDesktop} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
return (
<>
<ViewHeader title={_(msg`App Passwords`)} showOnDesktop />
<Text
type="sm"
style={[
styles.description,
pal.text,
isTabletOrDesktop && styles.descriptionDesktop,
]}>
<Trans>
Use app passwords to login to other Bluesky clients without giving
full access to your account or password.
</Trans>
</Text>
</>
)
}
function AppPassword({
testID,
name,
createdAt,
privileged,
}: {
testID: string
name: string
createdAt: string
privileged?: boolean
}) {
const pal = usePalette('default')
const {_, i18n} = useLingui()
const control = useDialogControl()
const deleteMutation = useAppPasswordDeleteMutation()
const onDelete = React.useCallback(async () => {
await deleteMutation.mutateAsync({name})
Toast.show(_(msg`App password deleted`))
}, [deleteMutation, name, _])
const onPress = React.useCallback(() => {
control.open()
}, [control])
return (
<TouchableOpacity
testID={testID}
style={[styles.item, pal.border]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={_(msg`Delete app password`)}
accessibilityHint="">
<View>
<Text type="md-bold" style={pal.text}>
{name}
</Text>
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
<Trans>
Created{' '}
{i18n.date(createdAt, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
</Trans>
</Text>
{privileged && (
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_xs]}>
<FontAwesomeIcon
icon="circle-exclamation"
color={pal.colors.textLight}
size={14}
/>
<Text type="md" style={pal.textLight}>
<Trans>Allows access to direct messages</Trans>
</Text>
</View>
)}
</View>
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
<Prompt.Basic
control={control}
title={_(msg`Delete app password?`)}
description={_(
msg`Are you sure you want to delete the app password "${name}"?`,
)}
onConfirm={onDelete}
confirmButtonCta={_(msg`Delete`)}
confirmButtonColor="negative"
/>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: 100,
},
containerDesktop: {
borderLeftWidth: 1,
borderRightWidth: 1,
paddingBottom: 0,
},
title: {
textAlign: 'center',
marginTop: 12,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 20,
marginBottom: 14,
},
descriptionDesktop: {
marginTop: 14,
},
scrollContainer: {
borderTopWidth: 1,
marginTop: 4,
marginBottom: 16,
},
flex1: {
flex: 1,
},
empty: {
paddingHorizontal: 20,
paddingVertical: 20,
borderRadius: 16,
marginHorizontal: 24,
marginTop: 10,
},
emptyText: {
textAlign: 'center',
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
paddingHorizontal: 20,
paddingVertical: 14,
},
pr10: {
marginRight: 10,
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
},
btnContainerDesktop: {
marginTop: 14,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
paddingHorizontal: 60,
paddingVertical: 14,
},
btnLabel: {
fontSize: 18,
},
trashIcon: {
color: 'red',
minWidth: 16,
},
})
-336
View File
@@ -1,336 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {s} from '#/lib/styles'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {useModalControls} from '#/state/modals'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {useSetMinimalShellMode} from '#/state/shell'
import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
export function LanguageSettingsScreen(props: Props) {
return IS_INTERNAL ? (
<NewLanguageSettingsScreen {...props} />
) : (
<LegacyLanguageSettingsScreen {...props} />
)
}
function LegacyLanguageSettingsScreen(_props: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const {isTabletOrDesktop} = useWebMediaQueries()
const setMinimalShellMode = useSetMinimalShellMode()
const {openModal} = useModalControls()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
const onPressContentLanguages = React.useCallback(() => {
openModal({name: 'content-languages-settings'})
}, [openModal])
const onChangePrimaryLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
if (!value) return
if (langPrefs.primaryLanguage !== value) {
setLangPrefs.setPrimaryLanguage(value)
}
},
[langPrefs, setLangPrefs],
)
const onChangeAppLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
if (!value) return
if (langPrefs.appLanguage !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
},
[langPrefs, setLangPrefs],
)
const myLanguages = React.useMemo(() => {
return (
langPrefs.contentLanguages
.map(lang => LANGUAGES.find(l => l.code2 === lang))
.filter(Boolean)
// @ts-ignore
.map(l => l.name)
.join(', ')
)
}, [langPrefs.contentLanguages])
return (
<Layout.Screen testID="PreferencesLanguagesScreen">
<CenteredView
style={[
pal.view,
pal.border,
styles.container,
isTabletOrDesktop && styles.desktopContainer,
]}>
<ViewHeader title={_(msg`Language Settings`)} showOnDesktop />
<View style={{paddingTop: 20, paddingHorizontal: 20}}>
{/* APP LANGUAGE */}
<View style={{paddingBottom: 20}}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>App Language</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Select your app language for the default text to display in the
app.
</Trans>
</Text>
<View style={{position: 'relative'}}>
<RNPickerSelect
placeholder={{}}
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
label: l.name,
value: l.code2,
key: l.code2,
}))}
style={{
inputAndroid: {
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
inputIOS: {
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
inputWeb: {
cursor: 'pointer',
// @ts-ignore web only
'-moz-appearance': 'none',
'-webkit-appearance': 'none',
appearance: 'none',
outline: 0,
borderWidth: 0,
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
}}
/>
<View
style={{
position: 'absolute',
top: 1,
right: 1,
bottom: 1,
width: 40,
backgroundColor: pal.viewLight.backgroundColor,
borderRadius: 24,
pointerEvents: 'none',
alignItems: 'center',
justifyContent: 'center',
}}>
<FontAwesomeIcon
icon="chevron-down"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: pal.border.borderColor,
marginBottom: 20,
}}
/>
{/* PRIMARY LANGUAGE */}
<View style={{paddingBottom: 20}}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Primary Language</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Select your preferred language for translations in your feed.
</Trans>
</Text>
<View style={{position: 'relative'}}>
<RNPickerSelect
placeholder={{}}
value={langPrefs.primaryLanguage}
onValueChange={onChangePrimaryLanguage}
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
label: l.name,
value: l.code2,
key: l.code2 + l.code3,
}))}
style={{
inputAndroid: {
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
inputIOS: {
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
inputWeb: {
cursor: 'pointer',
// @ts-ignore web only
'-moz-appearance': 'none',
'-webkit-appearance': 'none',
appearance: 'none',
outline: 0,
borderWidth: 0,
backgroundColor: pal.viewLight.backgroundColor,
color: pal.text.color,
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
},
}}
/>
<View
style={{
position: 'absolute',
top: 1,
right: 1,
bottom: 1,
width: 40,
backgroundColor: pal.viewLight.backgroundColor,
borderRadius: 24,
pointerEvents: 'none',
alignItems: 'center',
justifyContent: 'center',
}}>
<FontAwesomeIcon
icon="chevron-down"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
</View>
</View>
<View
style={{
height: 1,
backgroundColor: pal.border.borderColor,
marginBottom: 20,
}}
/>
{/* CONTENT LANGUAGES */}
<View style={{paddingBottom: 20}}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Content Languages</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Select which languages you want your subscribed feeds to
include. If none are selected, all languages will be shown.
</Trans>
</Text>
<Button
type="default"
onPress={onPressContentLanguages}
style={styles.button}>
<FontAwesomeIcon
icon={myLanguages.length ? 'check' : 'plus'}
style={pal.text as FontAwesomeIconStyle}
/>
<Text
type="button"
style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]}
numberOfLines={1}>
{myLanguages.length ? myLanguages : _(msg`Select languages`)}
</Text>
</Button>
</View>
</View>
</CenteredView>
</Layout.Screen>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: 90,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
paddingBottom: 40,
},
button: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
})
@@ -1,147 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
EmbedPlayerSource,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {
useExternalEmbedsPrefs,
useSetExternalEmbedPref,
} from '#/state/preferences'
import {useSetMinimalShellMode} from '#/state/shell'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesExternalEmbeds'
>
export function PreferencesExternalEmbeds(props: Props) {
return IS_INTERNAL ? (
<ExternalMediaPreferencesScreen {...props} />
) : (
<LegacyPreferencesExternalEmbeds {...props} />
)
}
function LegacyPreferencesExternalEmbeds({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrMobile} = useWebMediaQueries()
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
return (
<Layout.Screen testID="preferencesExternalEmbedsScreen">
<ScrollView
// @ts-ignore web only -prf
dataSet={{'stable-gutters': 1}}
contentContainerStyle={[pal.viewLight, {paddingBottom: 75}]}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
<View style={a.flex_1}>
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
<Trans>External Media Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Customize media from external sites.</Trans>
</Text>
</View>
</SimpleViewHeader>
<View style={[pal.view]}>
<View style={styles.infoCard}>
<Text style={pal.text}>
<Trans>
External media may allow websites to collect information about
you and your device. No information is sent or requested until
you press the "play" button.
</Trans>
</Text>
</View>
</View>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Enable media players for</Trans>
</Text>
{Object.entries(externalEmbedLabels)
// TODO: Remove special case when we disable the old integration.
.filter(([key]) => key !== 'tenor')
.map(([key, label]) => (
<PrefSelector
source={key as EmbedPlayerSource}
label={label}
key={key}
/>
))}
</ScrollView>
</Layout.Screen>
)
}
function PrefSelector({
source,
label,
}: {
source: EmbedPlayerSource
label: string
}) {
const pal = usePalette('default')
const setExternalEmbedPref = useSetExternalEmbedPref()
const sources = useExternalEmbedsPrefs()
return (
<View>
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={label}
labelType="lg"
isSelected={sources?.[source] === 'show'}
onPress={() =>
setExternalEmbedPref(
source,
sources?.[source] === 'show' ? 'hide' : 'show',
)
}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
heading: {
paddingHorizontal: 18,
paddingTop: 14,
paddingBottom: 14,
},
spacer: {
height: 8,
},
infoCard: {
paddingHorizontal: 20,
paddingVertical: 14,
},
toggleCard: {
paddingVertical: 8,
paddingHorizontal: 6,
marginBottom: 1,
},
})
@@ -1,249 +0,0 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {colors, s} from '#/lib/styles'
import {
usePreferencesQuery,
useSetFeedViewPreferencesMutation,
} from '#/state/queries/preferences'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'PreferencesFollowingFeed'
>
export function PreferencesFollowingFeed(props: Props) {
return IS_INTERNAL ? (
<FollowingFeedPreferencesScreen {...props} />
) : (
<LegacyPreferencesFollowingFeed {...props} />
)
}
function LegacyPreferencesFollowingFeed({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setFeedViewPref, variables} =
useSetFeedViewPreferencesMutation()
const showReplies = !(
variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
)
return (
<Layout.Screen testID="preferencesHomeFeedScreen">
<ScrollView
// @ts-ignore web only -sfn
dataSet={{'stable-gutters': 1}}
contentContainerStyle={{paddingBottom: 75}}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
<View style={a.flex_1}>
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
<Trans>Following Feed Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>
Fine-tune the content you see on your Following feed.
</Trans>
</Text>
</View>
</SimpleViewHeader>
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Show Replies</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Set this setting to "No" to hide all replies from your feed.
</Trans>
</Text>
<ToggleButton
testID="toggleRepliesBtn"
type="default-light"
label={showReplies ? _(msg`Yes`) : _(msg`No`)}
isSelected={showReplies}
onPress={() =>
setFeedViewPref({
hideReplies: !(
variables?.hideReplies ??
preferences?.feedViewPrefs?.hideReplies
),
})
}
/>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Show Reposts</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Set this setting to "No" to hide all reposts from your feed.
</Trans>
</Text>
<ToggleButton
type="default-light"
label={
variables?.hideReposts ??
preferences?.feedViewPrefs?.hideReposts
? _(msg`No`)
: _(msg`Yes`)
}
isSelected={
!(
variables?.hideReposts ??
preferences?.feedViewPrefs?.hideReposts
)
}
onPress={() =>
setFeedViewPref({
hideReposts: !(
variables?.hideReposts ??
preferences?.feedViewPrefs?.hideReposts
),
})
}
/>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Show Quote Posts</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Set this setting to "No" to hide all quote posts from your feed.
Reposts will still be visible.
</Trans>
</Text>
<ToggleButton
type="default-light"
label={
variables?.hideQuotePosts ??
preferences?.feedViewPrefs?.hideQuotePosts
? _(msg`No`)
: _(msg`Yes`)
}
isSelected={
!(
variables?.hideQuotePosts ??
preferences?.feedViewPrefs?.hideQuotePosts
)
}
onPress={() =>
setFeedViewPref({
hideQuotePosts: !(
variables?.hideQuotePosts ??
preferences?.feedViewPrefs?.hideQuotePosts
),
})
}
/>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
<Trans>Show Posts from My Feeds</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Set this setting to "Yes" to show samples of your saved feeds in
your Following feed. This is an experimental feature.
</Trans>
</Text>
<ToggleButton
type="default-light"
label={
variables?.lab_mergeFeedEnabled ??
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
? _(msg`Yes`)
: _(msg`No`)
}
isSelected={
!!(
variables?.lab_mergeFeedEnabled ??
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
)
}
onPress={() =>
setFeedViewPref({
lab_mergeFeedEnabled: !(
variables?.lab_mergeFeedEnabled ??
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
),
})
}
/>
</View>
</View>
</ScrollView>
</Layout.Screen>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
},
titleSection: {
paddingBottom: 30,
},
title: {
textAlign: 'center',
marginBottom: 5,
},
description: {
textAlign: 'center',
paddingHorizontal: 32,
},
cardsContainer: {
paddingHorizontal: 20,
paddingVertical: 16,
},
card: {
padding: 16,
borderRadius: 10,
marginBottom: 20,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
padding: 14,
backgroundColor: colors.blue3,
},
btnDesktop: {
marginHorizontal: 'auto',
paddingHorizontal: 80,
},
btnContainer: {
paddingTop: 20,
},
dimmed: {
opacity: 0.3,
},
})
-198
View File
@@ -1,198 +0,0 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {colors, s} from '#/lib/styles'
import {
usePreferencesQuery,
useSetThreadViewPreferencesMutation,
} from '#/state/queries/preferences'
import {RadioGroup} from '#/view/com/util/forms/RadioGroup'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {Text} from '#/view/com/util/text/Text'
import {ScrollView} from '#/view/com/util/Views'
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
import {atoms as a} from '#/alf'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
export function PreferencesThreads(props: Props) {
return IS_INTERNAL ? (
<ThreadPreferencesScreen {...props} />
) : (
<LegacyPreferencesThreads {...props} />
)
}
function LegacyPreferencesThreads({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries()
const {data: preferences} = usePreferencesQuery()
const {mutate: setThreadViewPrefs, variables} =
useSetThreadViewPreferencesMutation()
const prioritizeFollowedUsers = Boolean(
variables?.prioritizeFollowedUsers ??
preferences?.threadViewPrefs?.prioritizeFollowedUsers,
)
const treeViewEnabled = Boolean(
variables?.lab_treeViewEnabled ??
preferences?.threadViewPrefs?.lab_treeViewEnabled,
)
return (
<Layout.Screen testID="preferencesThreadsScreen">
<ScrollView
// @ts-ignore web only -prf
dataSet={{'stable-gutters': 1}}
contentContainerStyle={{paddingBottom: 75}}>
<SimpleViewHeader
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
<View style={a.flex_1}>
<Text type="title-lg" style={[pal.text, {fontWeight: '600'}]}>
<Trans>Thread Preferences</Trans>
</Text>
<Text style={pal.textLight}>
<Trans>Fine-tune the discussion threads.</Trans>
</Text>
</View>
</SimpleViewHeader>
{preferences ? (
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Sort Replies</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>Sort replies to the same post by:</Trans>
</Text>
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
<RadioGroup
type="default-light"
items={[
{key: 'oldest', label: _(msg`Oldest replies first`)},
{key: 'newest', label: _(msg`Newest replies first`)},
{
key: 'most-likes',
label: _(msg`Most-liked replies first`),
},
{
key: 'random',
label: _(msg`Random (aka "Poster's Roulette")`),
},
]}
onSelect={key => setThreadViewPrefs({sort: key})}
initialSelection={preferences?.threadViewPrefs?.sort}
/>
</View>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<Trans>Prioritize Your Follows</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Show replies by people you follow before all other replies.
</Trans>
</Text>
<ToggleButton
type="default-light"
label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)}
isSelected={prioritizeFollowedUsers}
onPress={() =>
setThreadViewPrefs({
prioritizeFollowedUsers: !prioritizeFollowedUsers,
})
}
/>
</View>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
<Trans>Threaded Mode</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
<Trans>
Set this setting to "Yes" to show replies in a threaded view.
This is an experimental feature.
</Trans>
</Text>
<ToggleButton
type="default-light"
label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)}
isSelected={treeViewEnabled}
onPress={() =>
setThreadViewPrefs({
lab_treeViewEnabled: !treeViewEnabled,
})
}
/>
</View>
</View>
) : (
<ActivityIndicator style={a.flex_1} />
)}
</ScrollView>
</Layout.Screen>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
desktopContainer: {
borderLeftWidth: 1,
borderRightWidth: 1,
},
titleSection: {
paddingBottom: 30,
},
title: {
textAlign: 'center',
marginBottom: 5,
},
description: {
textAlign: 'center',
paddingHorizontal: 32,
},
cardsContainer: {
paddingHorizontal: 20,
paddingVertical: 16,
},
card: {
padding: 16,
borderRadius: 10,
marginBottom: 20,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
padding: 14,
backgroundColor: colors.blue3,
},
btnDesktop: {
marginHorizontal: 'auto',
paddingHorizontal: 80,
},
btnContainer: {
paddingTop: 20,
},
dimmed: {
opacity: 0.3,
},
})
@@ -1,201 +0,0 @@
import React, {useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {useAgent, useSession} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography'
enum Stages {
Email,
ConfirmCode,
}
export function DisableEmail2FADialog({
control,
}: {
control: Dialog.DialogOuterProps['control']
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const agent = useAgent()
const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('')
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const onSendEmail = async () => {
setError('')
setIsProcessing(true)
try {
await agent.com.atproto.server.requestEmailUpdate()
setStage(Stages.ConfirmCode)
} catch (e) {
setError(cleanError(String(e)))
} finally {
setIsProcessing(false)
}
}
const onConfirmDisable = async () => {
setError('')
setIsProcessing(true)
try {
if (currentAccount?.email) {
await agent.com.atproto.server.updateEmail({
email: currentAccount!.email,
token: confirmationCode.trim(),
emailAuthFactor: false,
})
await agent.resumeSession(agent.session!)
Toast.show(_(msg`Email 2FA disabled`))
}
control.close()
} catch (e) {
const errMsg = String(e)
if (errMsg.includes('Token is invalid')) {
setError(_(msg`Invalid 2FA confirmation code.`))
} else {
setError(cleanError(errMsg))
}
} finally {
setIsProcessing(false)
}
}
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_md, a.w_full]}>
<Text
nativeID="dialog-title"
style={[a.text_2xl, a.font_bold, t.atoms.text]}>
<Trans>Disable Email 2FA</Trans>
</Text>
<P nativeID="dialog-description">
{stage === Stages.ConfirmCode ? (
<Trans>
An email has been sent to{' '}
{currentAccount?.email || '(no email)'}. It includes a
confirmation code which you can enter below.
</Trans>
) : (
<Trans>
To disable the email 2FA method, please verify your access to
the email address.
</Trans>
)}
</P>
{error ? <ErrorMessage message={error} /> : undefined}
{stage === Stages.Email ? (
<View
style={[
a.gap_sm,
gtMobile && [a.flex_row, a.justify_end, a.gap_md],
]}>
<Button
testID="sendEmailButton"
variant="solid"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onSendEmail}
label={_(msg`Send verification email`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Send verification email</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
<Button
testID="haveCodeButton"
variant="ghost"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={() => setStage(Stages.ConfirmCode)}
label={_(msg`I have a code`)}
disabled={isProcessing}>
<ButtonText>
<Trans>I have a code</Trans>
</ButtonText>
</Button>
</View>
) : stage === Stages.ConfirmCode ? (
<View>
<View style={[a.mb_md]}>
<TextField.LabelText>
<Trans>Confirmation code</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Lock} />
<Dialog.Input
testID="confirmationCode"
label={_(msg`Confirmation code`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="off"
value={confirmationCode}
onChangeText={setConfirmationCode}
onSubmitEditing={onConfirmDisable}
editable={!isProcessing}
/>
</TextField.Root>
</View>
<View
style={[
a.gap_sm,
gtMobile && [a.flex_row, a.justify_end, a.gap_md],
]}>
<Button
testID="resendCodeBtn"
variant="ghost"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onSendEmail}
label={_(msg`Resend email`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Resend email</Trans>
</ButtonText>
</Button>
<Button
testID="confirmBtn"
variant="solid"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onConfirmDisable}
label={_(msg`Confirm`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Confirm</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
) : undefined}
{!gtMobile && isNative && <View style={{height: 40}} />}
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -1,58 +0,0 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession} from '#/state/session'
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
import {useDialogControl} from '#/components/Dialog'
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
export function Email2FAToggle() {
const {_} = useLingui()
const {currentAccount} = useSession()
const {openModal} = useModalControls()
const disableDialogCtrl = useDialogControl()
const agent = useAgent()
const enableEmailAuthFactor = React.useCallback(async () => {
if (currentAccount?.email) {
await agent.com.atproto.server.updateEmail({
email: currentAccount.email,
emailAuthFactor: true,
})
await agent.resumeSession(agent.session!)
}
}, [currentAccount, agent])
const onToggle = React.useCallback(() => {
if (!currentAccount) {
return
}
if (currentAccount.emailAuthFactor) {
disableDialogCtrl.open()
} else {
if (!currentAccount.emailConfirmed) {
openModal({
name: 'verify-email',
onSuccess: enableEmailAuthFactor,
})
return
}
enableEmailAuthFactor()
}
}, [currentAccount, enableEmailAuthFactor, openModal, disableDialogCtrl])
return (
<>
<DisableEmail2FADialog control={disableDialogCtrl} />
<ToggleButton
type="default-light"
label={_(msg`Require email code to log into your account`)}
labelType="lg"
isSelected={!!currentAccount?.emailAuthFactor}
onPress={onToggle}
/>
</>
)
}
@@ -1,110 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {saveBytesToDisk} from '#/lib/media/manip'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ExportCarDialog({
control,
}: {
control: Dialog.DialogOuterProps['control']
}) {
const {_} = useLingui()
const t = useTheme()
const agent = useAgent()
const [loading, setLoading] = React.useState(false)
const download = React.useCallback(async () => {
if (!agent.session) {
return // shouldnt ever happen
}
try {
setLoading(true)
const did = agent.session.did
const downloadRes = await agent.com.atproto.sync.getRepo({did})
const saveRes = await saveBytesToDisk(
'repo.car',
downloadRes.data,
downloadRes.headers['content-type'],
)
if (saveRes) {
Toast.show(_(msg`File saved successfully!`))
}
} catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e})
Toast.show(_(msg`Error occurred while saving file`), 'xmark')
} finally {
setLoading(false)
control.close()
}
}, [_, control, agent])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_lg, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Export My Data</Trans>
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Trans>
Your account repository, containing all public data records, can
be downloaded as a "CAR" file. This file does not include media
embeds, such as images, or your private data, which must be
fetched separately.
</Trans>
</Text>
<Button
variant="solid"
color="primary"
size="large"
label={_(msg`Download CAR file`)}
disabled={loading}
onPress={download}>
<ButtonIcon icon={DownloadIcon} />
<ButtonText>
<Trans>Download CAR file</Trans>
</ButtonText>
{loading && <ButtonIcon icon={Loader} />}
</Button>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
a.flex_1,
]}>
<Trans>
This feature is in beta. You can read more about repository
exports in{' '}
<InlineLinkText
label={_(msg`View blogpost for more details`)}
to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}>
this blogpost
</InlineLinkText>
.
</Trans>
</Text>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
File diff suppressed because it is too large Load Diff