[Settings] Thread prefs revamp (#5772)
* thread preferences screen * minor tweaks * more spacing * replace gate with IS_INTERNAL * [Settings] Following feed prefs revamp (#5773) * gated new settings screen * Following feed prefs * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * replace pref following feed gate * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * use "Experimental" as the header --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] External media prefs revamp (#5774) * gated new settings screen * external media prefs revamp * replace gate ext media embeds * Update src/screens/Settings/ExternalMediaPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add imports for translation * alternate list style on native --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Languages revamp (partial) (#5775) * language settings (lazy restyle) * replace gate * fix text determining flex space * [Settings] App passwords revamp (#5777) * rework app passwords screen * Apply surfdude's copy changes Thanks @surfdude29! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * format * replace gate * use admonition for input error and animate --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Change handle dialog (#5781) * new change handle dialog * animations native only * overflow hidden on togglebutton animation * add a low-contrast border * extract out copybutton * finish change handle dialog * invalidate query on success * web fixes * error message for rate limit exceeded * typo * em dash! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * another em dash Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * set maxwidth of suffixtext * Copy tweak Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Notifs settings revamp (#5884) * rename, move, and restyle notif settings * bold "experimental:" --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import React, {useEffect, useMemo, useState} from 'react'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
LayoutAnimationConfig,
|
||||
LinearTransition,
|
||||
SlideInRight,
|
||||
SlideOutLeft,
|
||||
} from 'react-native-reanimated'
|
||||
import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextInput from '#/components/forms/TextField'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {CopyButton} from './CopyButton'
|
||||
|
||||
export function AddAppPasswordDialog({
|
||||
control,
|
||||
passwords,
|
||||
}: {
|
||||
control: Dialog.DialogControlProps
|
||||
passwords: string[]
|
||||
}) {
|
||||
const {height} = useWindowDimensions()
|
||||
return (
|
||||
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
|
||||
<Dialog.Handle />
|
||||
<CreateDialogInner passwords={passwords} />
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateDialogInner({passwords}: {passwords: string[]}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const autogeneratedName = useRandomName()
|
||||
const [name, setName] = useState('')
|
||||
const [privileged, setPrivileged] = useState(false)
|
||||
const {
|
||||
mutateAsync: actuallyCreateAppPassword,
|
||||
error: apiError,
|
||||
data,
|
||||
} = useAppPasswordCreateMutation()
|
||||
|
||||
const regexFailError = useMemo(
|
||||
() =>
|
||||
new DisplayableError(
|
||||
_(
|
||||
msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`,
|
||||
),
|
||||
),
|
||||
[_],
|
||||
)
|
||||
|
||||
const {
|
||||
mutate: createAppPassword,
|
||||
error: validationError,
|
||||
isPending,
|
||||
} = useMutation<
|
||||
ComAtprotoServerCreateAppPassword.AppPassword,
|
||||
Error | DisplayableError
|
||||
>({
|
||||
mutationFn: async () => {
|
||||
const chosenName = name.trim() || autogeneratedName
|
||||
if (chosenName.length < 4) {
|
||||
throw new DisplayableError(
|
||||
_(msg`App password names must be at least 4 characters long`),
|
||||
)
|
||||
}
|
||||
if (passwords.find(p => p === chosenName)) {
|
||||
throw new DisplayableError(_(msg`App password name must be unique`))
|
||||
}
|
||||
return await actuallyCreateAppPassword({name: chosenName, privileged})
|
||||
},
|
||||
})
|
||||
|
||||
const [hasBeenCopied, setHasBeenCopied] = useState(false)
|
||||
useEffect(() => {
|
||||
if (hasBeenCopied) {
|
||||
const timeout = setTimeout(() => setHasBeenCopied(false), 100)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [hasBeenCopied])
|
||||
|
||||
const error =
|
||||
validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError)
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Add app password`)}>
|
||||
<View style={[native(a.pt_md)]}>
|
||||
<LayoutAnimationConfig skipEntering skipExiting>
|
||||
{!data ? (
|
||||
<Animated.View
|
||||
style={[a.gap_lg]}
|
||||
exiting={native(SlideOutLeft)}
|
||||
key={0}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Add App Password</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Please enter a unique name for this app password or use our
|
||||
randomly generated one.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View>
|
||||
<TextInput.Root isInvalid={!!error}>
|
||||
<Dialog.Input
|
||||
label={_(msg`App Password`)}
|
||||
placeholder={autogeneratedName}
|
||||
onChangeText={setName}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={() => createAppPassword()}
|
||||
blurOnSubmit
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
autoFocus
|
||||
/>
|
||||
</TextInput.Root>
|
||||
</View>
|
||||
{error instanceof DisplayableError && (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Admonition type="error">{error.message}</Admonition>
|
||||
</Animated.View>
|
||||
)}
|
||||
<Animated.View
|
||||
style={[a.gap_lg]}
|
||||
layout={native(LinearTransition)}>
|
||||
<Toggle.Item
|
||||
name="privileged"
|
||||
type="checkbox"
|
||||
label={_(msg`Allow access to your direct messages`)}
|
||||
value={privileged}
|
||||
onChange={setPrivileged}
|
||||
style={[a.flex_1]}>
|
||||
<Toggle.Checkbox />
|
||||
<Toggle.LabelText
|
||||
style={[a.font_normal, a.text_md, a.leading_snug]}>
|
||||
<Trans>Allow access to your direct messages</Trans>
|
||||
</Toggle.LabelText>
|
||||
</Toggle.Item>
|
||||
<Button
|
||||
label={_(msg`Next`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
style={[a.flex_1]}
|
||||
onPress={() => createAppPassword()}
|
||||
disabled={isPending}>
|
||||
<ButtonText>
|
||||
<Trans>Next</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={ChevronRight} />
|
||||
</Button>
|
||||
{!!apiError ||
|
||||
(error && !(error instanceof DisplayableError) && (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
Failed to create app password. Please try again.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
</Animated.View>
|
||||
))}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View
|
||||
style={[a.gap_lg]}
|
||||
entering={isWeb ? FadeIn.delay(200) : SlideInRight}
|
||||
key={1}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Here is your app password!</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Use this to sign into the other app along with your handle.
|
||||
</Trans>
|
||||
</Text>
|
||||
<CopyButton
|
||||
value={data.password}
|
||||
label={_(msg`Copy App Password`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="secondary">
|
||||
<ButtonText>{data.password}</ButtonText>
|
||||
<ButtonIcon icon={CopyIcon} />
|
||||
</CopyButton>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
For security reasons, you won't be able to view this again. If
|
||||
you lose this app password, you'll need to generate a new one.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Done`)}
|
||||
size="large"
|
||||
variant="outline"
|
||||
color="primary"
|
||||
style={[a.flex_1]}
|
||||
onPress={() => control.close()}>
|
||||
<ButtonText>
|
||||
<Trans>Done</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</Animated.View>
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
class DisplayableError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'DisplayableError'
|
||||
}
|
||||
}
|
||||
|
||||
function useRandomName() {
|
||||
return useState(
|
||||
() => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
|
||||
)[0]
|
||||
}
|
||||
|
||||
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',
|
||||
]
|
||||
Reference in New Issue
Block a user