Improved server selector during account creation and signin (#2840)

* Replace the ServerInput modal with a new dialog based on alf that remembers your server address history and doesnt put staging and localdev in the options

* Update the server selector during account creation

* dont apply capitalization, use url keyboard

* Apply insets to dialog top

* Improve padding of dialogs on native

* Fix race condition in dialog close; also fix fire of the onClose event in dialogs

---------

Co-authored-by: Hailey <me@haileyok.com>
zio/stable
Paul Frazee 2024-02-12 13:36:20 -08:00 committed by GitHub
parent b91a6b429a
commit ba7463cadf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 316 additions and 267 deletions

View File

@ -52,6 +52,7 @@ export type ButtonProps = React.PropsWithChildren<
Pick<PressableProps, 'disabled' | 'onPress'> & Pick<PressableProps, 'disabled' | 'onPress'> &
AccessibilityProps & AccessibilityProps &
VariantProps & { VariantProps & {
testID?: string
label: string label: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
} }

View File

@ -34,6 +34,7 @@ export function Outer({
const sheet = React.useRef<BottomSheet>(null) const sheet = React.useRef<BottomSheet>(null)
const sheetOptions = nativeOptions?.sheet || {} const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets()
const open = React.useCallback<DialogControlProps['open']>((i = 0) => { const open = React.useCallback<DialogControlProps['open']>((i = 0) => {
sheet.current?.snapToIndex(i) sheet.current?.snapToIndex(i)
@ -41,8 +42,7 @@ export function Outer({
const close = React.useCallback(() => { const close = React.useCallback(() => {
sheet.current?.close() sheet.current?.close()
onClose?.() }, [])
}, [onClose])
useImperativeHandle( useImperativeHandle(
control.ref, control.ref,
@ -53,6 +53,15 @@ export function Outer({
[open, close], [open, close],
) )
const onChange = React.useCallback(
(index: number) => {
if (index === -1) {
onClose?.()
}
},
[onClose],
)
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
@ -63,6 +72,7 @@ export function Outer({
keyboardBehavior="interactive" keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize" android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore" keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions} {...sheetOptions}
ref={sheet} ref={sheet}
index={-1} index={-1}
@ -77,7 +87,7 @@ export function Outer({
)} )}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}} handleStyle={{display: 'none'}}
onClose={onClose}> onChange={onChange}>
<Context.Provider value={context}> <Context.Provider value={context}>
<View <View
style={[ style={[
@ -105,8 +115,8 @@ export function Inner(props: DialogInnerProps) {
<BottomSheetView <BottomSheetView
style={[ style={[
a.p_lg, a.p_lg,
a.pt_3xl,
{ {
paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
@ -121,11 +131,13 @@ export function ScrollableInner(props: DialogInnerProps) {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
return ( return (
<BottomSheetScrollView <BottomSheetScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
style={[ style={[
a.flex_1, // main diff is this a.flex_1, // main diff is this
a.p_lg, a.p_xl,
a.pt_3xl,
{ {
paddingTop: 40,
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
}, },
@ -139,21 +151,21 @@ export function ScrollableInner(props: DialogInnerProps) {
export function Handle() { export function Handle() {
const t = useTheme() const t = useTheme()
return ( return (
<View <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
style={[ <View
a.absolute, style={[
a.rounded_sm, a.rounded_sm,
a.z_10, {
{ top: a.pt_lg.paddingTop,
top: a.pt_lg.paddingTop, width: 35,
width: 35, height: 4,
height: 4, alignSelf: 'center',
alignSelf: 'center', backgroundColor: t.palette.contrast_900,
backgroundColor: t.palette.contrast_900, opacity: 0.5,
opacity: 0.5, },
}, ]}
]} />
/> </View>
) )
} }

View File

@ -238,10 +238,14 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput) export const Input = createInput(TextInput)
export function Label({children}: React.PropsWithChildren<{}>) { export function Label({
nativeID,
children,
}: React.PropsWithChildren<{nativeID?: string}>) {
const t = useTheme() const t = useTheme()
return ( return (
<Text <Text
nativeID={nativeID}
style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}>
{children} {children}
</Text> </Text>

View File

@ -8,7 +8,7 @@ import * as Toggle from '#/components/forms/Toggle'
export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> & export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
AccessibilityProps & AccessibilityProps &
React.PropsWithChildren<{}> React.PropsWithChildren<{testID?: string}>
export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & { export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & {
multiple?: boolean multiple?: boolean

View File

@ -26,12 +26,6 @@ export interface EditProfileModal {
onUpdate?: () => void onUpdate?: () => void
} }
export interface ServerInputModal {
name: 'server-input'
initialService: string
onSelect: (url: string) => void
}
export interface ModerationDetailsModal { export interface ModerationDetailsModal {
name: 'moderation-details' name: 'moderation-details'
context: 'account' | 'content' context: 'account' | 'content'
@ -222,7 +216,6 @@ export type Modal =
| AltTextImageModal | AltTextImageModal
| CropImageModal | CropImageModal
| EditImageModal | EditImageModal
| ServerInputModal
| RepostModal | RepostModal
| SelfLabelModal | SelfLabelModal
| ThreadgateModal | ThreadgateModal

View File

@ -112,6 +112,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
hiddenPosts: defaults.hiddenPosts, hiddenPosts: defaults.hiddenPosts,
externalEmbeds: defaults.externalEmbeds, externalEmbeds: defaults.externalEmbeds,
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed, lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
pdsAddressHistory: defaults.pdsAddressHistory,
} }
} }

View File

@ -57,6 +57,7 @@ export const schema = z.object({
hiddenPosts: z.array(z.string()).optional(), // should move to server hiddenPosts: z.array(z.string()).optional(), // should move to server
useInAppBrowser: z.boolean().optional(), useInAppBrowser: z.boolean().optional(),
lastSelectedHomeFeed: z.string().optional(), lastSelectedHomeFeed: z.string().optional(),
pdsAddressHistory: z.array(z.string()).optional(),
}) })
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
@ -91,4 +92,5 @@ export const defaults: Schema = {
hiddenPosts: [], hiddenPosts: [],
useInAppBrowser: undefined, useInAppBrowser: undefined,
lastSelectedHomeFeed: undefined, lastSelectedHomeFeed: undefined,
pdsAddressHistory: [],
} }

View File

@ -3,6 +3,7 @@ import {
ActivityIndicator, ActivityIndicator,
Keyboard, Keyboard,
StyleSheet, StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback, TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
@ -13,7 +14,6 @@ import {StepHeader} from './StepHeader'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from '../util/TextInput' import {TextInput} from '../util/TextInput'
import {Button} from '../../util/forms/Button'
import {Policies} from './Policies' import {Policies} from './Policies'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
@ -21,7 +21,14 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {logger} from '#/logger' import {logger} from '#/logger'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
import {toNiceDomain} from '#/lib/strings/url-helpers'
function sanitizeDate(date: Date): Date { function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') { if (!date || date.toString() === 'Invalid Date') {
@ -43,16 +50,12 @@ export function Step1({
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const serverInputControl = useDialogControl()
const onPressSelectService = React.useCallback(() => { const onPressSelectService = React.useCallback(() => {
openModal({ serverInputControl.open()
name: 'server-input',
initialService: uiState.serviceUrl,
onSelect: (url: string) =>
uiDispatch({type: 'set-service-url', value: url}),
})
Keyboard.dismiss() Keyboard.dismiss()
}, [uiDispatch, uiState.serviceUrl, openModal]) }, [serverInputControl])
const onPressWaitlist = React.useCallback(() => { const onPressWaitlist = React.useCallback(() => {
openModal({name: 'waitlist'}) openModal({name: 'waitlist'})
@ -64,23 +67,72 @@ export function Step1({
return ( return (
<View> <View>
<StepHeader uiState={uiState} title={_(msg`Your account`)}> <ServerInputDialog
<View> control={serverInputControl}
<Button onSelect={url => uiDispatch({type: 'set-service-url', value: url})}
testID="selectServiceButton" />
type="default" <StepHeader uiState={uiState} title={_(msg`Your account`)} />
style={{
aspectRatio: 1, <View style={s.pb20}>
justifyContent: 'center', <Text type="md-medium" style={[pal.text, s.mb2]}>
alignItems: 'center', <Trans>Hosting provider</Trans>
}} </Text>
accessibilityLabel={_(msg`Select service`)} <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}>
accessibilityHint={_(msg`Sets server for the Bluesky client`)} <View
onPress={onPressSelectService}> style={[
<FontAwesomeIcon icon="server" size={21} color={pal.colors.text} /> pal.borderDark,
</Button> {flexDirection: 'row', alignItems: 'center'},
]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, {marginLeft: 14}]}
/>
<TouchableOpacity
testID="loginSelectServiceButton"
style={{
flexDirection: 'row',
flex: 1,
alignItems: 'center',
}}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
<Text
type="xl"
style={[
pal.text,
{
flex: 1,
paddingVertical: 10,
paddingRight: 12,
paddingLeft: 10,
},
]}>
{toNiceDomain(uiState.serviceUrl)}
</Text>
<View
style={[
pal.btn,
{
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
</View>
</View> </View>
</StepHeader> </View>
{!uiState.serviceDescription ? ( {!uiState.serviceDescription ? (
<ActivityIndicator /> <ActivityIndicator />

View File

@ -1,6 +1,7 @@
import React, {useState, useEffect} from 'react' import React, {useState, useEffect} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Keyboard,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View, View,
@ -24,7 +25,9 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {styles} from './styles' import {styles} from './styles'
import {useModalControls} from '#/state/modals' import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@ -51,19 +54,16 @@ export const ForgotPasswordForm = ({
const [email, setEmail] = useState<string>('') const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls() const serverInputControl = useDialogControl()
useEffect(() => { useEffect(() => {
screen('Signin:ForgotPassword') screen('Signin:ForgotPassword')
}, [screen]) }, [screen])
const onPressSelectService = () => { const onPressSelectService = React.useCallback(() => {
openModal({ serverInputControl.open()
name: 'server-input', Keyboard.dismiss()
initialService: serviceUrl, }, [serverInputControl])
onSelect: setServiceUrl,
})
}
const onPressNext = async () => { const onPressNext = async () => {
if (!EmailValidator.validate(email)) { if (!EmailValidator.validate(email)) {
@ -96,6 +96,10 @@ export const ForgotPasswordForm = ({
return ( return (
<> <>
<View> <View>
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
/>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}> <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
<Trans>Reset password</Trans> <Trans>Reset password</Trans>
</Text> </Text>

View File

@ -25,7 +25,9 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {styles} from './styles' import {styles} from './styles'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals' import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../server-input'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@ -58,15 +60,11 @@ export const LoginForm = ({
const [password, setPassword] = useState<string>('') const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null) const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const {login} = useSessionApi() const {login} = useSessionApi()
const serverInputControl = useDialogControl()
const onPressSelectService = () => { const onPressSelectService = () => {
openModal({ serverInputControl.open()
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
Keyboard.dismiss() Keyboard.dismiss()
track('Signin:PressedSelectService') track('Signin:PressedSelectService')
} }
@ -130,6 +128,11 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password const isReady = !!serviceDescription && !!identifier && !!password
return ( return (
<View testID="loginForm"> <View testID="loginForm">
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
/>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}> <Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Sign into</Trans> <Trans>Sign into</Trans>
</Text> </Text>

View File

@ -0,0 +1,173 @@
import React from 'react'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {PROD_SERVICE} from 'lib/constants'
import * as persisted from '#/state/persisted'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Text, P} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import * as ToggleButton from '#/components/forms/ToggleButton'
import * as TextField from '#/components/forms/TextField'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
export function ServerInputDialog({
control,
onSelect,
}: {
control: Dialog.DialogOuterProps['control']
onSelect: (url: string) => void
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const [pdsAddressHistory, setPdsAddressHistory] = React.useState<string[]>(
persisted.get('pdsAddressHistory') || [],
)
const [fixedOption, setFixedOption] = React.useState([PROD_SERVICE])
const [customAddress, setCustomAddress] = React.useState('')
const onClose = React.useCallback(() => {
let url
if (fixedOption[0] === 'custom') {
url = customAddress.trim().toLowerCase()
if (!url) {
return
}
} else {
url = fixedOption[0]
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
if (url === 'localhost' || url.startsWith('localhost:')) {
url = `http://${url}`
} else {
url = `https://${url}`
}
}
if (fixedOption[0] === 'custom') {
if (!pdsAddressHistory.includes(url)) {
const newHistory = [url, ...pdsAddressHistory.slice(0, 4)]
setPdsAddressHistory(newHistory)
persisted.write('pdsAddressHistory', newHistory)
}
}
onSelect(url)
}, [
fixedOption,
customAddress,
onSelect,
pdsAddressHistory,
setPdsAddressHistory,
])
return (
<Dialog.Outer
control={control}
nativeOptions={{sheet: {snapPoints: ['100%']}}}
onClose={onClose}>
<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]}>
<Trans>Choose Service</Trans>
</Text>
<P nativeID="dialog-description" style={[a.text_sm]}>
<Trans>Select the service that hosts your data.</Trans>
</P>
<ToggleButton.Group
label="Preferences"
values={fixedOption}
onChange={setFixedOption}>
<ToggleButton.Button name={PROD_SERVICE} label={_(msg`Bluesky`)}>
{_(msg`Bluesky`)}
</ToggleButton.Button>
<ToggleButton.Button
testID="customSelectBtn"
name="custom"
label={_(msg`Custom`)}>
{_(msg`Custom`)}
</ToggleButton.Button>
</ToggleButton.Group>
{fixedOption[0] === 'custom' && (
<View
style={[
a.border,
t.atoms.border_contrast_low,
a.rounded_sm,
a.px_md,
a.py_md,
]}>
<TextField.Label nativeID="address-input-label">
<Trans>Server address</Trans>
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={Globe} />
<Dialog.Input
testID="customServerTextInput"
value={customAddress}
onChangeText={setCustomAddress}
label={_(msg`my-server.com`)}
accessibilityLabelledBy="address-input-label"
autoCapitalize="none"
keyboardType="url"
/>
</TextField.Root>
{pdsAddressHistory.length > 0 && (
<View style={[a.flex_row, a.flex_wrap, a.mt_xs]}>
{pdsAddressHistory.map(uri => (
<Button
key={uri}
variant="ghost"
color="primary"
label={uri}
style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
onPress={() => setCustomAddress(uri)}>
<ButtonText>{uri}</ButtonText>
</Button>
))}
</View>
)}
</View>
)}
<View style={[a.py_xs]}>
<P
style={[
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
a.flex_1,
]}>
<Trans>
Bluesky is an open network where you can choose your hosting
provider. Custom hosting is now available in beta for
developers.
</Trans>
</P>
</View>
<View style={gtMobile && [a.flex_row, a.justify_end]}>
<Button
testID="doneBtn"
variant="outline"
color="primary"
size="small"
onPress={() => control.close()}
label={_(msg`Done`)}>
{_(msg`Done`)}
</Button>
</View>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}

View File

@ -8,7 +8,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useModals, useModalControls} from '#/state/modals' import {useModals, useModalControls} from '#/state/modals'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel' import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate' import * as ThreadgateModal from './Threadgate'
@ -74,9 +73,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'edit-profile') { } else if (activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints snapPoints = EditProfileModal.snapPoints
element = <EditProfileModal.Component {...activeModal} /> element = <EditProfileModal.Component {...activeModal} />
} else if (activeModal?.name === 'server-input') {
snapPoints = ServerInputModal.snapPoints
element = <ServerInputModal.Component {...activeModal} />
} else if (activeModal?.name === 'report') { } else if (activeModal?.name === 'report') {
snapPoints = ReportModal.snapPoints snapPoints = ReportModal.snapPoints
element = <ReportModal.Component {...activeModal} /> element = <ReportModal.Component {...activeModal} />

View File

@ -9,7 +9,6 @@ import {useModals, useModalControls} from '#/state/modals'
import type {Modal as ModalIface} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ServerInputModal from './ServerInput'
import * as ReportModal from './report/Modal' import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel' import * as AppealLabelModal from './AppealLabel'
import * as CreateOrEditListModal from './CreateOrEditList' import * as CreateOrEditListModal from './CreateOrEditList'
@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ConfirmModal.Component {...modal} /> element = <ConfirmModal.Component {...modal} />
} else if (modal.name === 'edit-profile') { } else if (modal.name === 'edit-profile') {
element = <EditProfileModal.Component {...modal} /> element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'server-input') {
element = <ServerInputModal.Component {...modal} />
} else if (modal.name === 'report') { } else if (modal.name === 'report') {
element = <ReportModal.Component {...modal} /> element = <ReportModal.Component {...modal} />
} else if (modal.name === 'appeal-label') { } else if (modal.name === 'appeal-label') {

View File

@ -1,189 +0,0 @@
import React, {useState} from 'react'
import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%']
export function Component({onSelect}: {onSelect: (url: string) => void}) {
const theme = useTheme()
const pal = usePalette('default')
const [customUrl, setCustomUrl] = useState<string>('')
const {_} = useLingui()
const {closeModal} = useModalControls()
const doSelect = (url: string) => {
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `https://${url}`
}
closeModal()
onSelect(url)
}
return (
<View style={[pal.view, s.flex1]} testID="serverInputModal">
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
<Trans>Choose Service</Trans>
</Text>
<ScrollView style={styles.inner}>
<View style={styles.group}>
{LOGIN_INCLUDE_DEV_SERVERS ? (
<>
<TouchableOpacity
testID="localDevServerButton"
style={styles.btn}
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>
<Trans>Local dev server</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(STAGING_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>
<Trans>Staging</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
</>
) : undefined}
<TouchableOpacity
style={styles.btn}
onPress={() => doSelect(PROD_SERVICE)}
accessibilityRole="button"
accessibilityLabel={_(msg`Select Bluesky Social`)}
accessibilityHint="Sets Bluesky Social as your service provider">
<Text style={styles.btnText}>
<Trans>Bluesky.Social</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
/>
</TouchableOpacity>
</View>
<View style={styles.group}>
<Text style={[pal.text, styles.label]}>
<Trans>Other service</Trans>
</Text>
<View style={s.flexRow}>
<TextInput
testID="customServerTextInput"
style={[pal.borderDark, pal.text, styles.textInput]}
placeholder="e.g. https://bsky.app"
placeholderTextColor={colors.gray4}
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
value={customUrl}
onChangeText={setCustomUrl}
accessibilityLabel={_(msg`Custom domain`)}
// TODO: Simplify this wording further to be understandable by everyone
accessibilityHint={_(
msg`Use your domain as your Bluesky client service provider`,
)}
/>
<TouchableOpacity
testID="customServerSelectBtn"
style={[pal.borderDark, pal.text, styles.textInputBtn]}
onPress={() => doSelect(customUrl)}
accessibilityRole="button"
accessibilityLabel={`Confirm service. ${
customUrl === ''
? _(msg`Button disabled. Input custom domain to proceed.`)
: ''
}`}
accessibilityHint=""
// TODO - accessibility: Need to inform state change on failure
disabled={customUrl === ''}>
<FontAwesomeIcon
icon="check"
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}
size={18}
/>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
inner: {
padding: 14,
},
group: {
marginBottom: 20,
},
label: {
fontWeight: 'bold',
paddingHorizontal: 4,
paddingBottom: 4,
},
textInput: {
flex: 1,
borderWidth: 1,
borderTopLeftRadius: 6,
borderBottomLeftRadius: 6,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
},
textInputBtn: {
borderWidth: 1,
borderLeftWidth: 0,
borderTopRightRadius: 6,
borderBottomRightRadius: 6,
paddingHorizontal: 14,
paddingVertical: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.blue3,
borderRadius: 6,
paddingHorizontal: 14,
paddingVertical: 10,
marginBottom: 6,
},
btnText: {
flex: 1,
fontSize: 18,
fontWeight: '500',
color: colors.white,
},
checkIcon: {
position: 'relative',
...Platform.select({
android: {
top: 8,
},
ios: {
top: 2,
},
}),
},
})