Bluesky app fork with some witchin' additions 💫

alf the login form

+169 -154
+7 -1
src/components/forms/TextField.tsx
··· 14 14 import {Text} from '#/components/Typography' 15 15 import {useInteractionState} from '#/components/hooks/useInteractionState' 16 16 import {Props as SVGIconProps} from '#/components/icons/common' 17 + import {mergeRefs} from '#/lib/merge-refs' 17 18 18 19 const Context = React.createContext<{ 19 20 inputRef: React.RefObject<TextInput> | null ··· 128 129 value: string 129 130 onChangeText: (value: string) => void 130 131 isInvalid?: boolean 132 + inputRef?: React.RefObject<TextInput> 131 133 } 132 134 133 135 export function createInput(Component: typeof TextInput) { ··· 137 139 value, 138 140 onChangeText, 139 141 isInvalid, 142 + inputRef, 140 143 ...rest 141 144 }: InputProps) { 142 145 const t = useTheme() ··· 160 163 </Root> 161 164 ) 162 165 } 166 + 167 + const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) 163 168 164 169 return ( 165 170 <> ··· 167 172 accessibilityHint={undefined} 168 173 {...rest} 169 174 accessibilityLabel={label} 170 - ref={ctx.inputRef} 175 + ref={refs} 171 176 value={value} 172 177 onChangeText={onChangeText} 173 178 onFocus={ctx.onFocus} 174 179 onBlur={ctx.onBlur} 175 180 placeholder={placeholder || label} 176 181 placeholderTextColor={t.palette.contrast_500} 182 + keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 177 183 hitSlop={HITSLOP_20} 178 184 style={[ 179 185 a.relative,
+5
src/components/icons/Lock.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Lock_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z', 5 + })
+5
src/components/icons/Pencil.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Pencil_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M13.586 1.5a2 2 0 0 1 2.828 0L19.5 4.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 5.086 21H1a1 1 0 0 1-1-1v-4.086A2 2 0 0 1 .586 14.5l13-13ZM15 2.914l-13 13V19h3.086l13-13L15 2.914ZM11 20a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z', 5 + })
+50 -45
src/screens/Login/ChooseAccountForm.tsx
··· 13 13 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 14 14 import * as Toast from '#/view/com/util/Toast' 15 15 import {Button} from '#/components/Button' 16 - import {atoms as a, useTheme} from '#/alf' 16 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 17 17 import {Text} from '#/components/Typography' 18 18 import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' 19 19 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' ··· 106 106 const {accounts, currentAccount} = useSession() 107 107 const {initSession} = useSessionApi() 108 108 const {setShowLoggedOut} = useLoggedOutViewControls() 109 + const {gtMobile} = useBreakpoints() 109 110 110 111 React.useEffect(() => { 111 112 screen('Choose Account') ··· 133 134 134 135 return ( 135 136 <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> 136 - <Text style={[a.mt_md, a.mb_lg, a.font_bold]}> 137 - <Trans>Sign in as...</Trans> 138 - </Text> 139 - <Group> 140 - {accounts.map(account => ( 141 - <AccountItem 142 - key={account.did} 143 - account={account} 144 - onSelect={onSelect} 145 - isCurrentAccount={account.did === currentAccount?.did} 146 - /> 147 - ))} 148 - <TouchableOpacity 149 - testID="chooseNewAccountBtn" 150 - style={[a.flex_1]} 151 - onPress={() => onSelectAccount(undefined)} 152 - accessibilityRole="button" 153 - accessibilityLabel={_(msg`Login to account that is not listed`)} 154 - accessibilityHint=""> 155 - <View style={[a.flex_row, a.flex_row, a.align_center, {height: 48}]}> 156 - <Text 157 - style={[ 158 - a.align_baseline, 159 - a.flex_1, 160 - a.flex_row, 161 - a.py_sm, 162 - {paddingLeft: 48}, 163 - ]}> 164 - <Trans>Other account</Trans> 165 - </Text> 166 - <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> 167 - </View> 168 - </TouchableOpacity> 169 - </Group> 170 - <View style={[a.flex_row, a.mt_lg]}> 171 - <Button 172 - label={_(msg`Back`)} 173 - variant="solid" 174 - color="secondary" 175 - size="small" 176 - onPress={onPressBack}> 177 - <Trans>Back</Trans> 178 - </Button> 179 - <View style={[a.flex_1]} /> 137 + <View style={!gtMobile && a.px_lg}> 138 + <Text 139 + style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}> 140 + <Trans>Sign in as...</Trans> 141 + </Text> 142 + <Group> 143 + {accounts.map(account => ( 144 + <AccountItem 145 + key={account.did} 146 + account={account} 147 + onSelect={onSelect} 148 + isCurrentAccount={account.did === currentAccount?.did} 149 + /> 150 + ))} 151 + <TouchableOpacity 152 + testID="chooseNewAccountBtn" 153 + style={[a.flex_1]} 154 + onPress={() => onSelectAccount(undefined)} 155 + accessibilityRole="button" 156 + accessibilityLabel={_(msg`Login to account that is not listed`)} 157 + accessibilityHint=""> 158 + <View 159 + style={[a.flex_row, a.flex_row, a.align_center, {height: 48}]}> 160 + <Text 161 + style={[ 162 + a.align_baseline, 163 + a.flex_1, 164 + a.flex_row, 165 + a.py_sm, 166 + {paddingLeft: 48}, 167 + ]}> 168 + <Trans>Other account</Trans> 169 + </Text> 170 + <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> 171 + </View> 172 + </TouchableOpacity> 173 + </Group> 174 + <View style={[a.flex_row, a.mt_lg]}> 175 + <Button 176 + label={_(msg`Back`)} 177 + variant="solid" 178 + color="secondary" 179 + size="small" 180 + onPress={onPressBack}> 181 + {_(msg`Back`)} 182 + </Button> 183 + <View style={[a.flex_1]} /> 184 + </View> 180 185 </View> 181 186 </ScrollView> 182 187 )
+2 -2
src/screens/Login/index.tsx
··· 1 1 import React from 'react' 2 + import {KeyboardAvoidingView} from 'react-native' 2 3 import {useAnalytics} from '#/lib/analytics/analytics' 3 4 import {useLingui} from '@lingui/react' 4 5 import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' ··· 9 10 import {msg} from '@lingui/macro' 10 11 import {logger} from '#/logger' 11 12 import {atoms as a} from '#/alf' 12 - import {KeyboardAvoidingView} from 'react-native' 13 13 import {ChooseAccountForm} from './ChooseAccountForm' 14 14 import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm' 15 15 import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm' 16 16 import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm' 17 - import {LoginForm} from '#/view/com/auth/login/LoginForm' 17 + import {LoginForm} from '#/screens/Login/LoginForm' 18 18 19 19 enum Forms { 20 20 Login,
+99 -105
src/view/com/auth/login/LoginForm.tsx src/screens/Login/LoginForm.tsx
··· 6 6 TouchableOpacity, 7 7 View, 8 8 } from 'react-native' 9 - import { 10 - FontAwesomeIcon, 11 - FontAwesomeIconStyle, 12 - } from '@fortawesome/react-native-fontawesome' 13 9 import {ComAtprotoServerDescribeServer} from '@atproto/api' 10 + import {Trans, msg} from '@lingui/macro' 11 + 14 12 import {useAnalytics} from 'lib/analytics/analytics' 15 - import {Text} from '../../util/text/Text' 16 13 import {s} from 'lib/styles' 17 14 import {createFullHandle} from 'lib/strings/handles' 18 15 import {toNiceDomain} from 'lib/strings/url-helpers' 19 16 import {isNetworkError} from 'lib/strings/errors' 20 - import {usePalette} from 'lib/hooks/usePalette' 21 - import {useTheme} from 'lib/ThemeContext' 22 17 import {useSessionApi} from '#/state/session' 23 18 import {cleanError} from 'lib/strings/errors' 24 19 import {logger} from '#/logger' 25 - import {Trans, msg} from '@lingui/macro' 26 - import {styles} from './styles' 20 + import {styles} from '../../view/com/auth/login/styles' 27 21 import {useLingui} from '@lingui/react' 28 22 import {useDialogControl} from '#/components/Dialog' 29 - 30 - import {ServerInputDialog} from '../server-input' 23 + import {ServerInputDialog} from '../../view/com/auth/server-input' 24 + import {Button} from '#/components/Button' 25 + import {isAndroid} from '#/platform/detection' 26 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 27 + import {Text} from '#/components/Typography' 28 + import * as TextField from '#/components/forms/TextField' 29 + import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 30 + import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 31 + import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 32 + import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' 33 + import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 31 34 32 35 type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 33 36 ··· 40 43 setServiceUrl, 41 44 onPressRetryConnect, 42 45 onPressBack, 43 - onPressForgotPassword, 44 46 }: { 45 47 error: string 46 48 serviceUrl: string ··· 53 55 onPressForgotPassword: () => void 54 56 }) => { 55 57 const {track} = useAnalytics() 56 - const pal = usePalette('default') 57 - const theme = useTheme() 58 + const t = useTheme() 58 59 const [isProcessing, setIsProcessing] = useState<boolean>(false) 59 60 const [identifier, setIdentifier] = useState<string>(initialHandle) 60 61 const [password, setPassword] = useState<string>('') ··· 62 63 const {_} = useLingui() 63 64 const {login} = useSessionApi() 64 65 const serverInputControl = useDialogControl() 66 + const {gtMobile} = useBreakpoints() 65 67 66 68 const onPressSelectService = () => { 67 69 serverInputControl.open() ··· 127 129 128 130 const isReady = !!serviceDescription && !!identifier && !!password 129 131 return ( 130 - <View testID="loginForm"> 132 + <View testID="loginForm" style={[a.gap_lg, !gtMobile && a.px_lg]}> 131 133 <ServerInputDialog 132 134 control={serverInputControl} 133 135 onSelect={setServiceUrl} 134 136 /> 135 137 136 - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> 137 - <Trans>Sign into</Trans> 138 - </Text> 139 - <View style={[pal.borderDark, styles.group]}> 140 - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> 141 - <FontAwesomeIcon 142 - icon="globe" 143 - style={[pal.textLight, styles.groupContentIcon]} 144 - /> 145 - <TouchableOpacity 146 - testID="loginSelectServiceButton" 147 - style={styles.textBtn} 148 - onPress={onPressSelectService} 149 - accessibilityRole="button" 150 - accessibilityLabel={_(msg`Select service`)} 151 - accessibilityHint={_(msg`Sets server for the Bluesky client`)}> 152 - <Text type="xl" style={[pal.text, styles.textBtnLabel]}> 153 - {toNiceDomain(serviceUrl)} 154 - </Text> 155 - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> 156 - <FontAwesomeIcon 157 - icon="pen" 158 - size={12} 159 - style={pal.textLight as FontAwesomeIconStyle} 160 - /> 161 - </View> 162 - </TouchableOpacity> 163 - </View> 138 + <View> 139 + <TextField.Label> 140 + <Trans>Hosting provider</Trans> 141 + </TextField.Label> 142 + <TouchableOpacity 143 + accessibilityRole="button" 144 + style={[ 145 + a.w_full, 146 + a.flex_row, 147 + a.align_center, 148 + a.rounded_sm, 149 + a.px_md, 150 + a.gap_xs, 151 + {paddingVertical: isAndroid ? 14 : 9}, 152 + t.atoms.bg_contrast_25, 153 + ]} 154 + onPress={onPressSelectService}> 155 + <TextField.Icon icon={Globe} /> 156 + <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text> 157 + <View 158 + style={[ 159 + a.rounded_sm, 160 + t.atoms.bg_contrast_100, 161 + {marginLeft: 'auto', left: 6, padding: 6}, 162 + ]}> 163 + <Pencil 164 + style={{color: t.palette.contrast_500}} 165 + height={18} 166 + width={18} 167 + /> 168 + </View> 169 + </TouchableOpacity> 164 170 </View> 165 - <Text type="sm-bold" style={[pal.text, styles.groupLabel]}> 166 - <Trans>Account</Trans> 167 - </Text> 168 - <View style={[pal.borderDark, styles.group]}> 169 - <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> 170 - <FontAwesomeIcon 171 - icon="at" 172 - style={[pal.textLight, styles.groupContentIcon]} 173 - /> 174 - <TextInput 171 + <View> 172 + <TextField.Label> 173 + <Trans>Account</Trans> 174 + </TextField.Label> 175 + <TextField.Root> 176 + <TextField.Icon icon={At} /> 177 + <TextField.Input 175 178 testID="loginUsernameInput" 176 - style={[pal.text, styles.textInput]} 177 - placeholder={_(msg`Username or email address`)} 178 - placeholderTextColor={pal.colors.textLight} 179 + label={_(msg`Username or email address`)} 179 180 autoCapitalize="none" 180 181 autoFocus 181 182 autoCorrect={false} ··· 186 187 passwordInputRef.current?.focus() 187 188 }} 188 189 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 189 - keyboardAppearance={theme.colorScheme} 190 190 value={identifier} 191 191 onChangeText={str => 192 192 setIdentifier((str || '').toLowerCase().trim()) 193 193 } 194 194 editable={!isProcessing} 195 - accessibilityLabel={_(msg`Username or email address`)} 196 195 accessibilityHint={_( 197 196 msg`Input the username or email address you used at signup`, 198 197 )} 199 198 /> 200 - </View> 201 - <View style={[pal.borderDark, styles.groupContent]}> 202 - <FontAwesomeIcon 203 - icon="lock" 204 - style={[pal.textLight, styles.groupContentIcon]} 205 - /> 206 - <TextInput 199 + </TextField.Root> 200 + </View> 201 + <View> 202 + <TextField.Root> 203 + <TextField.Icon icon={Lock} /> 204 + <TextField.Input 207 205 testID="loginPasswordInput" 208 - ref={passwordInputRef} 209 - style={[pal.text, styles.textInput]} 210 - placeholder="Password" 211 - placeholderTextColor={pal.colors.textLight} 206 + inputRef={passwordInputRef} 207 + label={_(msg`Password`)} 212 208 autoCapitalize="none" 213 209 autoCorrect={false} 214 210 autoComplete="password" 215 211 returnKeyType="done" 216 212 enablesReturnKeyAutomatically={true} 217 - keyboardAppearance={theme.colorScheme} 218 213 secureTextEntry={true} 219 214 textContentType="password" 220 215 clearButtonMode="while-editing" ··· 223 218 onSubmitEditing={onPressNext} 224 219 blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing 225 220 editable={!isProcessing} 226 - accessibilityLabel={_(msg`Password`)} 227 221 accessibilityHint={ 228 222 identifier === '' 229 223 ? _(msg`Input your password`) 230 224 : _(msg`Input the password tied to ${identifier}`) 231 225 } 232 226 /> 233 - <TouchableOpacity 227 + {/* <TouchableOpacity 234 228 testID="forgotPasswordButton" 235 229 style={styles.textInputInnerBtn} 236 230 onPress={onPressForgotPassword} ··· 240 234 <Text style={pal.link}> 241 235 <Trans>Forgot</Trans> 242 236 </Text> 243 - </TouchableOpacity> 244 - </View> 237 + </TouchableOpacity> */} 238 + </TextField.Root> 245 239 </View> 246 240 {error ? ( 247 - <View style={styles.error}> 248 - <View style={styles.errorIcon}> 249 - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> 250 - </View> 251 - <View style={s.flex1}> 241 + <View style={[styles.error, {marginHorizontal: 0}]}> 242 + <Warning style={s.white} size="sm" /> 243 + <View style={(a.flex_1, a.ml_sm)}> 252 244 <Text style={[s.white, s.bold]}>{error}</Text> 253 245 </View> 254 246 </View> 255 247 ) : undefined} 256 - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> 257 - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> 258 - <Text type="xl" style={[pal.link, s.pl5]}> 259 - <Trans>Back</Trans> 260 - </Text> 261 - </TouchableOpacity> 248 + <View style={[a.flex_row, a.align_center]}> 249 + <Button 250 + label={_(msg`Back`)} 251 + variant="solid" 252 + color="secondary" 253 + size="small" 254 + onPress={onPressBack}> 255 + {_(msg`Back`)} 256 + </Button> 262 257 <View style={s.flex1} /> 263 258 {!serviceDescription && error ? ( 264 - <TouchableOpacity 259 + <Button 265 260 testID="loginRetryButton" 266 - onPress={onPressRetryConnect} 267 - accessibilityRole="button" 268 - accessibilityLabel={_(msg`Retry`)} 269 - accessibilityHint={_(msg`Retries login`)}> 270 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 271 - <Trans>Retry</Trans> 272 - </Text> 273 - </TouchableOpacity> 261 + label={_(msg`Retry`)} 262 + accessibilityHint={_(msg`Retries login`)} 263 + variant="solid" 264 + color="secondary" 265 + size="small" 266 + onPress={onPressRetryConnect}> 267 + {_(msg`Retry`)} 268 + </Button> 274 269 ) : !serviceDescription ? ( 275 270 <> 276 271 <ActivityIndicator /> 277 - <Text type="xl" style={[pal.textLight, s.pl10]}> 272 + <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 278 273 <Trans>Connecting...</Trans> 279 274 </Text> 280 275 </> 281 276 ) : isProcessing ? ( 282 277 <ActivityIndicator /> 283 278 ) : isReady ? ( 284 - <TouchableOpacity 285 - testID="loginNextButton" 286 - onPress={onPressNext} 287 - accessibilityRole="button" 288 - accessibilityLabel={_(msg`Go to next`)} 289 - accessibilityHint={_(msg`Navigates to the next screen`)}> 290 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 291 - <Trans>Next</Trans> 292 - </Text> 293 - </TouchableOpacity> 279 + <Button 280 + label={_(msg`Next`)} 281 + accessibilityHint={_(msg`Navigates to the next screen`)} 282 + variant="solid" 283 + color="primary" 284 + size="small" 285 + onPress={onPressNext}> 286 + {_(msg`Next`)} 287 + </Button> 294 288 ) : undefined} 295 289 </View> 296 290 </View>
+1 -1
src/view/com/auth/server-input/index.tsx
··· 67 67 return ( 68 68 <Dialog.Outer 69 69 control={control} 70 - nativeOptions={{sheet: {snapPoints: ['100%']}}} 70 + nativeOptions={{sheet: {snapPoints: ['80', '100%']}}} 71 71 onClose={onClose}> 72 72 <Dialog.Handle /> 73 73