diff options
-rw-r--r-- | src/components/Button.tsx | 1 | ||||
-rw-r--r-- | src/components/Dialog/index.tsx | 54 | ||||
-rw-r--r-- | src/components/forms/TextField.tsx | 6 | ||||
-rw-r--r-- | src/components/forms/ToggleButton.tsx | 2 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 7 | ||||
-rw-r--r-- | src/state/persisted/legacy.ts | 1 | ||||
-rw-r--r-- | src/state/persisted/schema.ts | 2 | ||||
-rw-r--r-- | src/view/com/auth/create/Step1.tsx | 102 | ||||
-rw-r--r-- | src/view/com/auth/login/ForgotPasswordForm.tsx | 22 | ||||
-rw-r--r-- | src/view/com/auth/login/LoginForm.tsx | 17 | ||||
-rw-r--r-- | src/view/com/auth/server-input/index.tsx | 173 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/ServerInput.tsx | 189 |
14 files changed, 316 insertions, 267 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index f88fbcbde..68cee4374 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -52,6 +52,7 @@ export type ButtonProps = React.PropsWithChildren< Pick<PressableProps, 'disabled' | 'onPress'> & AccessibilityProps & VariantProps & { + testID?: string label: string style?: StyleProp<ViewStyle> } diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index 44e4dc8a7..9132e68de 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -34,6 +34,7 @@ export function Outer({ const sheet = React.useRef<BottomSheet>(null) const sheetOptions = nativeOptions?.sheet || {} const hasSnapPoints = !!sheetOptions.snapPoints + const insets = useSafeAreaInsets() const open = React.useCallback<DialogControlProps['open']>((i = 0) => { sheet.current?.snapToIndex(i) @@ -41,8 +42,7 @@ export function Outer({ const close = React.useCallback(() => { sheet.current?.close() - onClose?.() - }, [onClose]) + }, []) useImperativeHandle( control.ref, @@ -53,6 +53,15 @@ export function Outer({ [open, close], ) + const onChange = React.useCallback( + (index: number) => { + if (index === -1) { + onClose?.() + } + }, + [onClose], + ) + const context = React.useMemo(() => ({close}), [close]) return ( @@ -63,6 +72,7 @@ export function Outer({ keyboardBehavior="interactive" android_keyboardInputMode="adjustResize" keyboardBlurBehavior="restore" + topInset={insets.top} {...sheetOptions} ref={sheet} index={-1} @@ -77,7 +87,7 @@ export function Outer({ )} handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} handleStyle={{display: 'none'}} - onClose={onClose}> + onChange={onChange}> <Context.Provider value={context}> <View style={[ @@ -105,8 +115,8 @@ export function Inner(props: DialogInnerProps) { <BottomSheetView style={[ a.p_lg, - a.pt_3xl, { + paddingTop: 40, borderTopLeftRadius: 40, borderTopRightRadius: 40, paddingBottom: insets.bottom + a.pb_5xl.paddingBottom, @@ -121,11 +131,13 @@ export function ScrollableInner(props: DialogInnerProps) { const insets = useSafeAreaInsets() return ( <BottomSheetScrollView + keyboardShouldPersistTaps="handled" + keyboardDismissMode="on-drag" style={[ a.flex_1, // main diff is this - a.p_lg, - a.pt_3xl, + a.p_xl, { + paddingTop: 40, borderTopLeftRadius: 40, borderTopRightRadius: 40, }, @@ -139,21 +151,21 @@ export function ScrollableInner(props: DialogInnerProps) { export function Handle() { const t = useTheme() return ( - <View - style={[ - a.absolute, - a.rounded_sm, - a.z_10, - { - top: a.pt_lg.paddingTop, - width: 35, - height: 4, - alignSelf: 'center', - backgroundColor: t.palette.contrast_900, - opacity: 0.5, - }, - ]} - /> + <View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}> + <View + style={[ + a.rounded_sm, + { + top: a.pt_lg.paddingTop, + width: 35, + height: 4, + alignSelf: 'center', + backgroundColor: t.palette.contrast_900, + opacity: 0.5, + }, + ]} + /> + </View> ) } diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx index 70f900bb9..99d5e7152 100644 --- a/src/components/forms/TextField.tsx +++ b/src/components/forms/TextField.tsx @@ -238,10 +238,14 @@ export function createInput(Component: typeof TextInput) { export const Input = createInput(TextInput) -export function Label({children}: React.PropsWithChildren<{}>) { +export function Label({ + nativeID, + children, +}: React.PropsWithChildren<{nativeID?: string}>) { const t = useTheme() return ( <Text + nativeID={nativeID} style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium, a.mb_sm]}> {children} </Text> diff --git a/src/components/forms/ToggleButton.tsx b/src/components/forms/ToggleButton.tsx index 90790f9fc..7e1bd70b9 100644 --- a/src/components/forms/ToggleButton.tsx +++ b/src/components/forms/ToggleButton.tsx @@ -8,7 +8,7 @@ import * as Toggle from '#/components/forms/Toggle' export type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> & AccessibilityProps & - React.PropsWithChildren<{}> + React.PropsWithChildren<{testID?: string}> export type GroupProps = Omit<Toggle.GroupProps, 'style' | 'type'> & { multiple?: boolean diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 096211bd4..691add005 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -26,12 +26,6 @@ export interface EditProfileModal { onUpdate?: () => void } -export interface ServerInputModal { - name: 'server-input' - initialService: string - onSelect: (url: string) => void -} - export interface ModerationDetailsModal { name: 'moderation-details' context: 'account' | 'content' @@ -222,7 +216,6 @@ export type Modal = | AltTextImageModal | CropImageModal | EditImageModal - | ServerInputModal | RepostModal | SelfLabelModal | ThreadgateModal diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts index cb4b5b1a9..cce080c84 100644 --- a/src/state/persisted/legacy.ts +++ b/src/state/persisted/legacy.ts @@ -112,6 +112,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema { hiddenPosts: defaults.hiddenPosts, externalEmbeds: defaults.externalEmbeds, lastSelectedHomeFeed: defaults.lastSelectedHomeFeed, + pdsAddressHistory: defaults.pdsAddressHistory, } } diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 6771ee6e4..0aefaa474 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -57,6 +57,7 @@ export const schema = z.object({ hiddenPosts: z.array(z.string()).optional(), // should move to server useInAppBrowser: z.boolean().optional(), lastSelectedHomeFeed: z.string().optional(), + pdsAddressHistory: z.array(z.string()).optional(), }) export type Schema = z.infer<typeof schema> @@ -91,4 +92,5 @@ export const defaults: Schema = { hiddenPosts: [], useInAppBrowser: undefined, lastSelectedHomeFeed: undefined, + pdsAddressHistory: [], } diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index a2663da86..94e03ff7a 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -3,6 +3,7 @@ import { ActivityIndicator, Keyboard, StyleSheet, + TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native' @@ -13,7 +14,6 @@ import {StepHeader} from './StepHeader' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {TextInput} from '../util/TextInput' -import {Button} from '../../util/forms/Button' import {Policies} from './Policies' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {isWeb} from 'platform/detection' @@ -21,7 +21,14 @@ import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' 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 { if (!date || date.toString() === 'Invalid Date') { @@ -43,16 +50,12 @@ export function Step1({ const pal = usePalette('default') const {_} = useLingui() const {openModal} = useModalControls() + const serverInputControl = useDialogControl() const onPressSelectService = React.useCallback(() => { - openModal({ - name: 'server-input', - initialService: uiState.serviceUrl, - onSelect: (url: string) => - uiDispatch({type: 'set-service-url', value: url}), - }) + serverInputControl.open() Keyboard.dismiss() - }, [uiDispatch, uiState.serviceUrl, openModal]) + }, [serverInputControl]) const onPressWaitlist = React.useCallback(() => { openModal({name: 'waitlist'}) @@ -64,23 +67,72 @@ export function Step1({ return ( <View> - <StepHeader uiState={uiState} title={_(msg`Your account`)}> - <View> - <Button - testID="selectServiceButton" - type="default" - style={{ - aspectRatio: 1, - justifyContent: 'center', - alignItems: 'center', - }} - accessibilityLabel={_(msg`Select service`)} - accessibilityHint={_(msg`Sets server for the Bluesky client`)} - onPress={onPressSelectService}> - <FontAwesomeIcon icon="server" size={21} color={pal.colors.text} /> - </Button> + <ServerInputDialog + control={serverInputControl} + onSelect={url => uiDispatch({type: 'set-service-url', value: url})} + /> + <StepHeader uiState={uiState} title={_(msg`Your account`)} /> + + <View style={s.pb20}> + <Text type="md-medium" style={[pal.text, s.mb2]}> + <Trans>Hosting provider</Trans> + </Text> + <View style={[pal.border, {borderWidth: 1, borderRadius: 6}]}> + <View + style={[ + pal.borderDark, + {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> - </StepHeader> + </View> {!uiState.serviceDescription ? ( <ActivityIndicator /> diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx index 79399d85d..322da2b8f 100644 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ b/src/view/com/auth/login/ForgotPasswordForm.tsx @@ -1,6 +1,7 @@ import React, {useState, useEffect} from 'react' import { ActivityIndicator, + Keyboard, TextInput, TouchableOpacity, View, @@ -24,7 +25,9 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {styles} from './styles' -import {useModalControls} from '#/state/modals' +import {useDialogControl} from '#/components/Dialog' + +import {ServerInputDialog} from '../server-input' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -51,19 +54,16 @@ export const ForgotPasswordForm = ({ const [email, setEmail] = useState<string>('') const {screen} = useAnalytics() const {_} = useLingui() - const {openModal} = useModalControls() + const serverInputControl = useDialogControl() useEffect(() => { screen('Signin:ForgotPassword') }, [screen]) - const onPressSelectService = () => { - openModal({ - name: 'server-input', - initialService: serviceUrl, - onSelect: setServiceUrl, - }) - } + const onPressSelectService = React.useCallback(() => { + serverInputControl.open() + Keyboard.dismiss() + }, [serverInputControl]) const onPressNext = async () => { if (!EmailValidator.validate(email)) { @@ -96,6 +96,10 @@ export const ForgotPasswordForm = ({ return ( <> <View> + <ServerInputDialog + control={serverInputControl} + onSelect={setServiceUrl} + /> <Text type="title-lg" style={[pal.text, styles.screenTitle]}> <Trans>Reset password</Trans> </Text> diff --git a/src/view/com/auth/login/LoginForm.tsx b/src/view/com/auth/login/LoginForm.tsx index 10608a54b..e480de7a4 100644 --- a/src/view/com/auth/login/LoginForm.tsx +++ b/src/view/com/auth/login/LoginForm.tsx @@ -25,7 +25,9 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {styles} from './styles' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' +import {useDialogControl} from '#/components/Dialog' + +import {ServerInputDialog} from '../server-input' type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -58,15 +60,11 @@ export const LoginForm = ({ const [password, setPassword] = useState<string>('') const passwordInputRef = useRef<TextInput>(null) const {_} = useLingui() - const {openModal} = useModalControls() const {login} = useSessionApi() + const serverInputControl = useDialogControl() const onPressSelectService = () => { - openModal({ - name: 'server-input', - initialService: serviceUrl, - onSelect: setServiceUrl, - }) + serverInputControl.open() Keyboard.dismiss() track('Signin:PressedSelectService') } @@ -130,6 +128,11 @@ export const LoginForm = ({ const isReady = !!serviceDescription && !!identifier && !!password return ( <View testID="loginForm"> + <ServerInputDialog + control={serverInputControl} + onSelect={setServiceUrl} + /> + <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> <Trans>Sign into</Trans> </Text> diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx new file mode 100644 index 000000000..a70621973 --- /dev/null +++ b/src/view/com/auth/server-input/index.tsx @@ -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> + ) +} diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index decdc6535..8da91c75c 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -8,7 +8,6 @@ import {usePalette} from 'lib/hooks/usePalette' import {useModals, useModalControls} from '#/state/modals' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' -import * as ServerInputModal from './ServerInput' import * as RepostModal from './Repost' import * as SelfLabelModal from './SelfLabel' import * as ThreadgateModal from './Threadgate' @@ -74,9 +73,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'edit-profile') { snapPoints = EditProfileModal.snapPoints element = <EditProfileModal.Component {...activeModal} /> - } else if (activeModal?.name === 'server-input') { - snapPoints = ServerInputModal.snapPoints - element = <ServerInputModal.Component {...activeModal} /> } else if (activeModal?.name === 'report') { snapPoints = ReportModal.snapPoints element = <ReportModal.Component {...activeModal} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index cb6f5bead..97a60be91 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -9,7 +9,6 @@ import {useModals, useModalControls} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' -import * as ServerInputModal from './ServerInput' import * as ReportModal from './report/Modal' import * as AppealLabelModal from './AppealLabel' import * as CreateOrEditListModal from './CreateOrEditList' @@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <ConfirmModal.Component {...modal} /> } else if (modal.name === 'edit-profile') { element = <EditProfileModal.Component {...modal} /> - } else if (modal.name === 'server-input') { - element = <ServerInputModal.Component {...modal} /> } else if (modal.name === 'report') { element = <ReportModal.Component {...modal} /> } else if (modal.name === 'appeal-label') { diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx deleted file mode 100644 index 550dffa1c..000000000 --- a/src/view/com/modals/ServerInput.tsx +++ /dev/null @@ -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, - }, - }), - }, -}) |