diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/modals/Modal.tsx | 8 | ||||
-rw-r--r-- | src/view/com/modals/ServerInput.tsx | 140 | ||||
-rw-r--r-- | src/view/index.ts | 4 | ||||
-rw-r--r-- | src/view/lib/strings.ts | 13 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/Login.tsx | 322 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 2 |
7 files changed, 360 insertions, 135 deletions
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index f2c61a6ae..210cdc41f 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -13,6 +13,7 @@ import * as SharePostModal from './SharePost.native' import * as EditProfileModal from './EditProfile' import * as CreateSceneModal from './CreateScene' import * as InviteToSceneModal from './InviteToScene' +import * as ServerInputModal from './ServerInput' const CLOSED_SNAPPOINTS = ['10%'] @@ -77,6 +78,13 @@ export const Modal = observer(function Modal() { {...(store.shell.activeModal as models.InviteToSceneModel)} /> ) + } else if (store.shell.activeModal?.name === 'server-input') { + snapPoints = ServerInputModal.snapPoints + element = ( + <ServerInputModal.Component + {...(store.shell.activeModal as models.ServerInputModel)} + /> + ) } else { element = <View /> } diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx new file mode 100644 index 000000000..1f3cc90f9 --- /dev/null +++ b/src/view/com/modals/ServerInput.tsx @@ -0,0 +1,140 @@ +import React, {useState} from 'react' +import Toast from '../util/Toast' +import {StyleSheet, Text, TextInput, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import LinearGradient from 'react-native-linear-gradient' +import {ErrorMessage} from '../util/ErrorMessage' +import {useStores} from '../../../state' +import {ProfileViewModel} from '../../../state/models/profile-view' +import {s, colors, gradients} from '../../lib/styles' +import {enforceLen, MAX_DISPLAY_NAME, MAX_DESCRIPTION} from '../../lib/strings' +import { + IS_PROD_BUILD, + LOCAL_DEV_SERVICE, + STAGING_SERVICE, + PROD_SERVICE, +} from '../../../state/index' + +export const snapPoints = ['80%'] + +export function Component({ + initialService, + onSelect, +}: { + initialService: string + onSelect: (url: string) => void +}) { + const store = useStores() + const [customUrl, setCustomUrl] = useState<string>('') + + const doSelect = (url: string) => { + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = `https://${url}` + } + store.shell.closeModal() + onSelect(url) + } + + return ( + <View style={s.flex1}> + <Text style={[s.textCenter, s.bold, s.f18]}>Choose Service</Text> + <View style={styles.inner}> + <View style={styles.group}> + {!IS_PROD_BUILD ? ( + <> + <TouchableOpacity + style={styles.btn} + onPress={() => doSelect(LOCAL_DEV_SERVICE)}> + <Text style={styles.btnText}>Local dev server</Text> + <FontAwesomeIcon icon="arrow-right" style={s.white} /> + </TouchableOpacity> + <TouchableOpacity + style={styles.btn} + onPress={() => doSelect(STAGING_SERVICE)}> + <Text style={styles.btnText}>Staging</Text> + <FontAwesomeIcon icon="arrow-right" style={s.white} /> + </TouchableOpacity> + </> + ) : undefined} + <TouchableOpacity + style={styles.btn} + onPress={() => doSelect(PROD_SERVICE)}> + <Text style={styles.btnText}>Bluesky.Social</Text> + <FontAwesomeIcon icon="arrow-right" style={s.white} /> + </TouchableOpacity> + </View> + <View style={styles.group}> + <Text style={styles.label}>Other service</Text> + <View style={{flexDirection: 'row'}}> + <TextInput + style={styles.textInput} + placeholder="e.g. https://bsky.app" + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + value={customUrl} + onChangeText={setCustomUrl} + /> + <TouchableOpacity + style={styles.textInputBtn} + onPress={() => doSelect(customUrl)}> + <FontAwesomeIcon + icon="check" + style={[s.black, {position: 'relative', top: 2}]} + size={18} + /> + </TouchableOpacity> + </View> + </View> + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + inner: { + padding: 14, + }, + group: { + marginBottom: 20, + }, + label: { + fontWeight: 'bold', + paddingHorizontal: 4, + paddingBottom: 4, + }, + textInput: { + flex: 1, + borderWidth: 1, + borderColor: colors.gray3, + borderTopLeftRadius: 6, + borderBottomLeftRadius: 6, + paddingHorizontal: 14, + paddingVertical: 12, + fontSize: 16, + }, + textInputBtn: { + borderWidth: 1, + borderLeftWidth: 0, + borderColor: colors.gray3, + 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, + }, +}) diff --git a/src/view/index.ts b/src/view/index.ts index 341051d4e..78361e75b 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -5,6 +5,7 @@ import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown' import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft' +import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight' import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons' import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket' import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare' @@ -35,6 +36,7 @@ import {faLock} from '@fortawesome/free-solid-svg-icons/faLock' import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import {faMessage} from '@fortawesome/free-regular-svg-icons/faMessage' import {faNoteSticky} from '@fortawesome/free-solid-svg-icons/faNoteSticky' +import {faPen} from '@fortawesome/free-solid-svg-icons/faPen' import {faPenNib} from '@fortawesome/free-solid-svg-icons/faPenNib' import {faPenToSquare} from '@fortawesome/free-solid-svg-icons/faPenToSquare' import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' @@ -59,6 +61,7 @@ export function setup() { faAngleLeft, faAngleRight, faArrowLeft, + faArrowRight, faArrowRightFromBracket, faArrowUpFromBracket, faArrowUpRightFromSquare, @@ -89,6 +92,7 @@ export function setup() { faMagnifyingGlass, faMessage, faNoteSticky, + faPen, faPenNib, faPenToSquare, faPlus, diff --git a/src/view/lib/strings.ts b/src/view/lib/strings.ts index 214bb51d6..05b23331f 100644 --- a/src/view/lib/strings.ts +++ b/src/view/lib/strings.ts @@ -1,5 +1,6 @@ import {AtUri} from '../../third-party/uri' import {Entity} from '../../third-party/api/src/client/types/app/bsky/feed/post' +import {PROD_SERVICE} from '../../state' export const MAX_DISPLAY_NAME = 64 export const MAX_DESCRIPTION = 256 @@ -106,3 +107,15 @@ export function cleanError(str: string): string { } return str } + +export function toNiceDomain(url: string): string { + try { + const urlp = new URL(url) + if (`https://${urlp.host}` === PROD_SERVICE) { + return 'Bluesky.Social' + } + return urlp.host + } catch (e) { + return url + } +} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 036f7d148..04ce2d0cb 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -8,7 +8,6 @@ import {useStores} from '../../state' import {FeedModel} from '../../state/models/feed-view' import {ScreenParams} from '../routes' import {s} from '../lib/styles' -import {BUILD} from '../../env' export const Home = observer(function Home({ visible, @@ -57,10 +56,7 @@ export const Home = observer(function Home({ return ( <View style={s.flex1}> - <ViewHeader - title="Bluesky" - subtitle={`Private Beta${BUILD !== 'prod' ? ` [${BUILD}]` : ''}`} - /> + <ViewHeader title="Bluesky" subtitle="Private Beta" /> <Feed key="default" feed={defaultFeedView} diff --git a/src/view/screens/Login.tsx b/src/view/screens/Login.tsx index ac93613eb..328a56e9a 100644 --- a/src/view/screens/Login.tsx +++ b/src/view/screens/Login.tsx @@ -2,6 +2,7 @@ import React, {useState, useEffect} from 'react' import { ActivityIndicator, KeyboardAvoidingView, + ScrollView, StyleSheet, Text, TextInput, @@ -15,10 +16,10 @@ import * as EmailValidator from 'email-validator' import {observer} from 'mobx-react-lite' import {Picker} from '../com/util/Picker' import {s, colors} from '../lib/styles' -import {makeValidHandle, createFullHandle} from '../lib/strings' +import {makeValidHandle, createFullHandle, toNiceDomain} from '../lib/strings' import {useStores, DEFAULT_SERVICE} from '../../state' import {ServiceDescription} from '../../state/models/session' -import {BUILD} from '../../env' +import {ServerInputModel} from '../../state/models/shell-ui' enum ScreenState { SigninOrCreateAccount, @@ -72,9 +73,7 @@ const SigninOrCreateAccount = ({ <View style={styles.hero}> <Logo /> <Text style={styles.title}>Bluesky</Text> - <Text style={styles.subtitle}> - [ private beta {BUILD !== 'prod' ? `- ${BUILD} ` : ''}] - </Text> + <Text style={styles.subtitle}>[ private beta ]</Text> </View> <View style={s.flex1}> <TouchableOpacity style={styles.btn} onPress={onPressCreateAccount}> @@ -112,6 +111,7 @@ const SigninOrCreateAccount = ({ const Signin = ({onPressBack}: {onPressBack: () => void}) => { const store = useStores() const [isProcessing, setIsProcessing] = useState<boolean>(false) + const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE) const [serviceDescription, setServiceDescription] = useState< ServiceDescription | undefined >(undefined) @@ -121,10 +121,9 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { useEffect(() => { let aborted = false - if (serviceDescription || error) { - return - } - store.session.describeService(DEFAULT_SERVICE).then( + setError('') + console.log('Fetching service description', serviceUrl) + store.session.describeService(serviceUrl).then( desc => { if (aborted) return setServiceDescription(desc) @@ -140,7 +139,11 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { return () => { aborted = true } - }, []) + }, [serviceUrl]) + + const onPressSelectService = () => { + store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl)) + } const onPressNext = async () => { setError('') @@ -168,7 +171,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { } await store.session.login({ - service: DEFAULT_SERVICE, + service: serviceUrl, handle: fullHandle, password, }) @@ -194,9 +197,14 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { <Logo /> </View> <View style={styles.group}> - <View style={styles.groupTitle}> - <Text style={[s.white, s.f18, s.bold]}>Sign in</Text> - </View> + <TouchableOpacity + style={styles.groupTitle} + onPress={onPressSelectService}> + <Text style={[s.white, s.f18, s.bold]} numberOfLines={1}> + Sign in to {toNiceDomain(serviceUrl)} + </Text> + <FontAwesomeIcon icon="pen" size={10} style={styles.groupTitleIcon} /> + </TouchableOpacity> {error ? ( <View style={styles.error}> <View style={styles.errorIcon}> @@ -256,6 +264,7 @@ const Signin = ({onPressBack}: {onPressBack: () => void}) => { const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { const store = useStores() const [isProcessing, setIsProcessing] = useState<boolean>(false) + const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE) const [error, setError] = useState<string>('') const [serviceDescription, setServiceDescription] = useState< ServiceDescription | undefined @@ -268,10 +277,9 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { useEffect(() => { let aborted = false - if (serviceDescription || error) { - return - } - store.session.describeService(DEFAULT_SERVICE).then( + setError('') + console.log('Fetching service description', serviceUrl) + store.session.describeService(serviceUrl).then( desc => { if (aborted) return setServiceDescription(desc) @@ -288,7 +296,11 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { return () => { aborted = true } - }, []) + }, [serviceUrl]) + + const onPressSelectService = () => { + store.shell.openModal(new ServerInputModel(serviceUrl, setServiceUrl)) + } const onPressNext = async () => { if (!email) { @@ -307,7 +319,7 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { setIsProcessing(true) try { await store.session.createAccount({ - service: DEFAULT_SERVICE, + service: serviceUrl, email, handle: createFullHandle(handle, userDomain), password, @@ -346,136 +358,164 @@ const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => { ) return ( - <KeyboardAvoidingView behavior="padding" style={{flex: 1}}> - <View style={styles.logoHero}> - <Logo /> - </View> - {serviceDescription ? ( - <> - {error ? ( - <View style={[styles.error, styles.errorFloating]}> - <View style={styles.errorIcon}> - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> + <ScrollView style={{flex: 1}}> + <KeyboardAvoidingView behavior="padding" style={{flex: 1}}> + <View style={styles.logoHero}> + <Logo /> + </View> + {serviceDescription ? ( + <> + {error ? ( + <View style={[styles.error, styles.errorFloating]}> + <View style={styles.errorIcon}> + <FontAwesomeIcon + icon="exclamation" + style={s.white} + size={10} + /> + </View> + <View style={s.flex1}> + <Text style={[s.white, s.bold]}>{error}</Text> + </View> </View> - <View style={s.flex1}> - <Text style={[s.white, s.bold]}>{error}</Text> + ) : undefined} + <View style={[styles.group]}> + <View style={styles.groupTitle}> + <Text style={[s.white, s.f18, s.bold]}> + Create a new account + </Text> </View> - </View> - ) : undefined} - <View style={styles.group}> - <View style={styles.groupTitle}> - <Text style={[s.white, s.f18, s.bold]}>Create a new account</Text> - </View> - {serviceDescription?.inviteCodeRequired ? ( + <View style={styles.groupContent}> + <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} /> + <TouchableOpacity + style={styles.textBtn} + onPress={onPressSelectService}> + <Text style={styles.textBtnLabel}> + {toNiceDomain(serviceUrl)} + </Text> + <FontAwesomeIcon + icon="pen" + size={12} + style={styles.textBtnIcon} + /> + </TouchableOpacity> + </View> + {serviceDescription?.inviteCodeRequired ? ( + <View style={styles.groupContent}> + <FontAwesomeIcon + icon="ticket" + style={styles.groupContentIcon} + /> + <TextInput + style={[styles.textInput]} + placeholder="Invite code" + placeholderTextColor={colors.blue0} + autoCapitalize="none" + autoCorrect={false} + autoFocus + value={inviteCode} + onChangeText={setInviteCode} + editable={!isProcessing} + /> + </View> + ) : undefined} <View style={styles.groupContent}> <FontAwesomeIcon - icon="ticket" + icon="envelope" style={styles.groupContentIcon} /> <TextInput style={[styles.textInput]} - placeholder="Invite code" + placeholder="Email address" placeholderTextColor={colors.blue0} autoCapitalize="none" autoCorrect={false} - autoFocus - value={inviteCode} - onChangeText={setInviteCode} + value={email} + onChangeText={setEmail} + editable={!isProcessing} + /> + </View> + <View style={styles.groupContent}> + <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} /> + <TextInput + style={[styles.textInput]} + placeholder="Choose your password" + placeholderTextColor={colors.blue0} + autoCapitalize="none" + autoCorrect={false} + secureTextEntry + value={password} + onChangeText={setPassword} editable={!isProcessing} /> </View> - ) : undefined} - <View style={styles.groupContent}> - <FontAwesomeIcon - icon="envelope" - style={styles.groupContentIcon} - /> - <TextInput - style={[styles.textInput]} - placeholder="Email address" - placeholderTextColor={colors.blue0} - autoCapitalize="none" - autoCorrect={false} - value={email} - onChangeText={setEmail} - editable={!isProcessing} - /> - </View> - <View style={styles.groupContent}> - <FontAwesomeIcon icon="lock" style={styles.groupContentIcon} /> - <TextInput - style={[styles.textInput]} - placeholder="Choose your password" - placeholderTextColor={colors.blue0} - autoCapitalize="none" - autoCorrect={false} - secureTextEntry - value={password} - onChangeText={setPassword} - editable={!isProcessing} - /> - </View> - </View> - <View style={styles.group}> - <View style={styles.groupTitle}> - <Text style={[s.white, s.f18, s.bold]}>Choose your username</Text> - </View> - <View style={styles.groupContent}> - <FontAwesomeIcon icon="at" style={styles.groupContentIcon} /> - <TextInput - style={[styles.textInput]} - placeholder="eg alice" - placeholderTextColor={colors.blue0} - autoCapitalize="none" - value={handle} - onChangeText={v => setHandle(makeValidHandle(v))} - editable={!isProcessing} - /> </View> - {serviceDescription.availableUserDomains.length > 1 && ( + <View style={styles.group}> + <View style={styles.groupTitle}> + <Text style={[s.white, s.f18, s.bold]}> + Choose your username + </Text> + </View> <View style={styles.groupContent}> - <FontAwesomeIcon icon="globe" style={styles.groupContentIcon} /> - <Picker - style={styles.picker} - labelStyle={styles.pickerLabel} - iconStyle={styles.pickerIcon} - value={userDomain} - items={serviceDescription.availableUserDomains.map(d => ({ - label: `.${d}`, - value: d, - }))} - onChange={itemValue => setUserDomain(itemValue)} - enabled={!isProcessing} + <FontAwesomeIcon icon="at" style={styles.groupContentIcon} /> + <TextInput + style={[styles.textInput]} + placeholder="eg alice" + placeholderTextColor={colors.blue0} + autoCapitalize="none" + value={handle} + onChangeText={v => setHandle(makeValidHandle(v))} + editable={!isProcessing} /> </View> - )} - <View style={styles.groupContent}> - <Text style={[s.white, s.p10]}> - Your full username will be{' '} - <Text style={s.bold}> - @{createFullHandle(handle, userDomain)} + {serviceDescription.availableUserDomains.length > 1 && ( + <View style={styles.groupContent}> + <FontAwesomeIcon + icon="globe" + style={styles.groupContentIcon} + /> + <Picker + style={styles.picker} + labelStyle={styles.pickerLabel} + iconStyle={styles.pickerIcon} + value={userDomain} + items={serviceDescription.availableUserDomains.map(d => ({ + label: `.${d}`, + value: d, + }))} + onChange={itemValue => setUserDomain(itemValue)} + enabled={!isProcessing} + /> + </View> + )} + <View style={styles.groupContent}> + <Text style={[s.white, s.p10]}> + Your full username will be{' '} + <Text style={s.bold}> + @{createFullHandle(handle, userDomain)} + </Text> </Text> - </Text> + </View> </View> - </View> - <View style={[s.flexRow, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBack}> - <Text style={[s.white, s.f18, s.pl5]}>Back</Text> - </TouchableOpacity> - <View style={s.flex1} /> - <TouchableOpacity onPress={onPressNext}> - {isProcessing ? ( - <ActivityIndicator color="#fff" /> - ) : ( - <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text> - )} - </TouchableOpacity> - </View> - </> - ) : ( - <InitialLoadView /> - )} - </KeyboardAvoidingView> + <View style={[s.flexRow, s.pl20, s.pr20, {paddingBottom: 200}]}> + <TouchableOpacity onPress={onPressBack}> + <Text style={[s.white, s.f18, s.pl5]}>Back</Text> + </TouchableOpacity> + <View style={s.flex1} /> + <TouchableOpacity onPress={onPressNext}> + {isProcessing ? ( + <ActivityIndicator color="#fff" /> + ) : ( + <Text style={[s.white, s.f18, s.bold, s.pr5]}>Next</Text> + )} + </TouchableOpacity> + </View> + </> + ) : ( + <InitialLoadView /> + )} + </KeyboardAvoidingView> + </ScrollView> ) } @@ -577,9 +617,15 @@ const styles = StyleSheet.create({ backgroundColor: colors.blue3, }, groupTitle: { + flexDirection: 'row', + alignItems: 'center', paddingVertical: 8, paddingHorizontal: 12, }, + groupTitleIcon: { + color: colors.white, + marginHorizontal: 6, + }, groupContent: { borderTopWidth: 1, borderTopColor: colors.blue1, @@ -600,6 +646,22 @@ const styles = StyleSheet.create({ fontSize: 18, borderRadius: 10, }, + textBtn: { + flexDirection: 'row', + flex: 1, + alignItems: 'center', + }, + textBtnLabel: { + flex: 1, + color: colors.white, + paddingVertical: 10, + paddingHorizontal: 12, + fontSize: 18, + }, + textBtnIcon: { + color: colors.white, + marginHorizontal: 12, + }, picker: { flex: 1, width: '100%', diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 712d6dc23..96390e9b8 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -170,6 +170,7 @@ export const MobileShell: React.FC = observer(() => { <SafeAreaView style={styles.innerContainer}> <Login /> </SafeAreaView> + <Modal /> </LinearGradient> ) } @@ -294,6 +295,7 @@ function constructScreenRenderDesc(nav: NavigationModel): { const styles = StyleSheet.create({ outerContainer: { height: '100%', + flex: 1, }, innerContainer: { flex: 1, |