diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/modals/AddAppPasswords.tsx | 216 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/ViewHeader.tsx | 25 | ||||
-rw-r--r-- | src/view/screens/AppPasswords.tsx | 275 | ||||
-rw-r--r-- | src/view/screens/PostLikedBy.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/PostRepostedBy.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/ProfileFollowers.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/ProfileFollows.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 16 |
9 files changed, 538 insertions, 6 deletions
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx new file mode 100644 index 000000000..1d2f80ff0 --- /dev/null +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -0,0 +1,216 @@ +import React, {useState} from 'react' +import {StyleSheet, TextInput, View, TouchableOpacity} from 'react-native' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {s} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import Clipboard from '@react-native-clipboard/clipboard' +import * as Toast from '../util/Toast' + +export const snapPoints = ['70%'] + +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', +] + +export function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [name, setName] = useState( + shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], + ) + const [appPassword, setAppPassword] = useState<string>() + const [wasCopied, setWasCopied] = useState(false) + + const onCopy = React.useCallback(() => { + if (appPassword) { + Clipboard.setString(appPassword) + Toast.show('Copied to clipboard') + setWasCopied(true) + } + }, [appPassword]) + + const onDone = React.useCallback(() => { + store.shell.closeModal() + }, [store]) + + const createAppPassword = async () => { + try { + const newPassword = await store.me.createAppPassword(name) + if (newPassword) { + setAppPassword(newPassword.password) + } else { + Toast.show('Failed to create app password.') + // TODO: better error handling (?) + } + } catch (e) { + Toast.show('Failed to create app password.') + store.log.error('Failed to create app password', {e}) + } + } + + return ( + <View style={[styles.container, pal.view]} testID="addAppPasswordsModal"> + <View> + {!appPassword ? ( + <Text type="lg"> + Please enter a unique name for this App Password. We have generated + a random name for you. + </Text> + ) : ( + <Text type="lg"> + <Text type="lg-bold">Here is your app password.</Text> Use this to + sign into the other app along with your handle. + </Text> + )} + {!appPassword ? ( + <View style={[pal.btn, styles.textInputWrapper]}> + <TextInput + style={[styles.input, pal.text]} + onChangeText={setName} + value={name} + placeholder="Enter a name for this App Password" + placeholderTextColor={pal.colors.textLight} + autoCorrect={false} + autoComplete="off" + autoCapitalize="none" + autoFocus={true} + selectTextOnFocus={true} + multiline={true} // need this to be true otherwise selectTextOnFocus doesn't work + numberOfLines={1} // hack for multiline so only one line shows (android) + scrollEnabled={false} // hack for multiline so only one line shows (ios) + blurOnSubmit={true} // hack for multiline so it submits + editable={!appPassword} + returnKeyType="done" + onEndEditing={createAppPassword} + /> + </View> + ) : ( + <TouchableOpacity + style={[pal.border, styles.passwordContainer, pal.btn]} + onPress={onCopy}> + <Text type="2xl-bold">{appPassword}</Text> + {wasCopied ? ( + <Text style={[pal.textLight]}>Copied</Text> + ) : ( + <FontAwesomeIcon + icon={['far', 'clone']} + style={pal.text as FontAwesomeIconStyle} + size={18} + /> + )} + </TouchableOpacity> + )} + </View> + {appPassword ? ( + <Text type="lg" style={[pal.textLight, s.mb10]}> + For security reasons, you won't be able to view this again. If you + lose this password, you'll need to generate a new one. + </Text> + ) : null} + <View style={styles.btnContainer}> + <Button + type="primary" + label={!appPassword ? 'Create App Password' : 'Done'} + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={!appPassword ? createAppPassword : onDone} + /> + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 50, + marginHorizontal: 16, + }, + textInputWrapper: { + borderRadius: 8, + flexDirection: 'row', + alignItems: 'center', + marginTop: 16, + marginBottom: 8, + }, + input: { + flex: 1, + width: '100%', + paddingVertical: 10, + paddingHorizontal: 8, + marginTop: 6, + fontSize: 17, + letterSpacing: 0.25, + fontWeight: '400', + borderRadius: 10, + }, + passwordContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 8, + paddingHorizontal: 16, + alignItems: 'center', + borderRadius: 10, + marginTop: 16, + marginBottom: 12, + }, + btnContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 12, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + paddingHorizontal: 60, + paddingVertical: 14, + }, + btnLabel: { + fontSize: 18, + }, + groupContent: { + borderTopWidth: 1, + flexDirection: 'row', + alignItems: 'center', + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index a83cdfdae..5d034a19d 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -17,6 +17,7 @@ import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' +import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' const DEFAULT_SNAPPOINTS = ['90%'] @@ -81,6 +82,9 @@ export const ModalsContainer = observer(function ModalsContainer() { } else if (activeModal?.name === 'invite-codes') { snapPoints = InviteCodesModal.snapPoints element = <InviteCodesModal.Component /> + } else if (activeModal?.name === 'add-app-password') { + snapPoints = AddAppPassword.snapPoints + element = <AddAppPassword.Component /> } else if (activeModal?.name === 'content-filtering-settings') { snapPoints = ContentFilteringSettingsModal.snapPoints element = <ContentFilteringSettingsModal.Component /> diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index ad0a5a1d2..816c835cc 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -3,6 +3,7 @@ import {observer} from 'mobx-react-lite' import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' +import {CenteredView} from './Views' import {UserAvatar} from './UserAvatar' import {Text} from './text/Text' import {useStores} from 'state/index' @@ -18,10 +19,12 @@ export const ViewHeader = observer(function ({ title, canGoBack, hideOnScroll, + showOnDesktop, }: { title: string canGoBack?: boolean hideOnScroll?: boolean + showOnDesktop?: boolean }) { const pal = usePalette('default') const store = useStores() @@ -42,7 +45,10 @@ export const ViewHeader = observer(function ({ }, [track, store]) if (isDesktopWeb) { - return <></> + if (showOnDesktop) { + return <DesktopWebHeader title={title} /> + } + return null } else { if (typeof canGoBack === 'undefined') { canGoBack = navigation.canGoBack() @@ -76,6 +82,19 @@ export const ViewHeader = observer(function ({ } }) +function DesktopWebHeader({title}: {title: string}) { + const pal = usePalette('default') + return ( + <CenteredView style={[styles.header, styles.desktopHeader, pal.border]}> + <View style={styles.titleContainer} pointerEvents="none"> + <Text type="title-lg" style={[pal.text, styles.title]}> + {title} + </Text> + </View> + </CenteredView> + ) +} + const Container = observer( ({ children, @@ -133,6 +152,10 @@ const styles = StyleSheet.create({ top: 0, width: '100%', }, + desktopHeader: { + borderBottomWidth: 1, + paddingVertical: 12, + }, titleContainer: { marginLeft: 'auto', diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx new file mode 100644 index 000000000..c3e837f84 --- /dev/null +++ b/src/view/screens/AppPasswords.tsx @@ -0,0 +1,275 @@ +import React from 'react' +import {Alert, StyleSheet, TouchableOpacity, View} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {ScrollView} from 'react-native-gesture-handler' +import {Text} from '../com/util/text/Text' +import {Button} from '../com/util/forms/Button' +import * as Toast from '../com/util/Toast' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {isDesktopWeb} from 'platform/detection' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {observer} from 'mobx-react-lite' +import {NativeStackScreenProps} from '@react-navigation/native-stack' +import {CommonNavigatorParams} from 'lib/routes/types' +import {useAnalytics} from 'lib/analytics' +import {useFocusEffect} from '@react-navigation/native' +import {ViewHeader} from '../com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> +export const AppPasswords = withAuthRequired( + observer(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const {screen} = useAnalytics() + + useFocusEffect( + React.useCallback(() => { + screen('Settings') + store.shell.setMinimalShellMode(false) + }, [screen, store]), + ) + + const onAdd = React.useCallback(async () => { + store.shell.openModal({name: 'add-app-password'}) + }, [store]) + + // no app passwords (empty) state + if (store.me.appPasswords.length === 0) { + return ( + <CenteredView + style={[ + styles.container, + isDesktopWeb && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="appPasswordsScreen"> + <AppPasswordsHeader /> + <View style={[styles.empty, pal.viewLight]}> + <Text type="lg" style={[pal.text, styles.emptyText]}> + You have not created any app passwords yet. You can create one by + pressing the button below. + </Text> + </View> + {!isDesktopWeb && <View style={styles.flex1} />} + <View + style={[ + styles.btnContainer, + isDesktopWeb && styles.btnContainerDesktop, + ]}> + <Button + testID="appPasswordBtn" + type="primary" + label="Add App Password" + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={onAdd} + /> + </View> + </CenteredView> + ) + } + + // has app passwords + return ( + <CenteredView + style={[ + styles.container, + isDesktopWeb && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="appPasswordsScreen"> + <AppPasswordsHeader /> + <ScrollView + style={[ + styles.scrollContainer, + pal.border, + !isDesktopWeb && styles.flex1, + ]}> + {store.me.appPasswords.map((password, i) => ( + <AppPassword + key={password.name} + testID={`appPassword-${i}`} + name={password.name} + createdAt={password.createdAt} + /> + ))} + {isDesktopWeb && ( + <View style={[styles.btnContainer, styles.btnContainerDesktop]}> + <Button + testID="appPasswordBtn" + type="primary" + label="Add App Password" + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={onAdd} + /> + </View> + )} + </ScrollView> + {!isDesktopWeb && ( + <View style={styles.btnContainer}> + <Button + testID="appPasswordBtn" + type="primary" + label="Add App Password" + style={styles.btn} + labelStyle={styles.btnLabel} + onPress={onAdd} + /> + </View> + )} + </CenteredView> + ) + }), +) + +function AppPasswordsHeader() { + const pal = usePalette('default') + return ( + <> + <ViewHeader title="App Passwords" showOnDesktop /> + <Text + type="sm" + style={[ + styles.description, + pal.text, + isDesktopWeb && styles.descriptionDesktop, + ]}> + These passwords can be used to log onto Bluesky in other apps without + giving them full access to your account or your password. + </Text> + </> + ) +} + +function AppPassword({ + testID, + name, + createdAt, +}: { + testID: string + name: string + createdAt: string +}) { + const pal = usePalette('default') + const store = useStores() + + const onDelete = React.useCallback(async () => { + Alert.alert( + 'Delete App Password', + `Are you sure you want to delete the app password "${name}"?`, + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + await store.me.deleteAppPassword(name) + Toast.show('App password deleted') + }, + }, + ], + ) + }, [store, name]) + + return ( + <TouchableOpacity + testID={testID} + style={[styles.item, pal.border]} + onPress={onDelete}> + <Text type="md-bold" style={pal.text}> + {name} + </Text> + <View style={styles.flex1} /> + <Text type="md" style={[pal.text, styles.pr10]}> + {new Date(createdAt).toDateString()} + </Text> + <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> + </TouchableOpacity> + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + title: { + textAlign: 'center', + marginTop: 12, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 20, + marginBottom: 14, + }, + descriptionDesktop: { + marginTop: 14, + }, + + scrollContainer: { + borderTopWidth: 1, + marginTop: 4, + marginBottom: 16, + }, + + flex1: { + flex: 1, + }, + empty: { + paddingHorizontal: 20, + paddingVertical: 20, + borderRadius: 16, + marginHorizontal: 24, + marginTop: 10, + }, + emptyText: { + textAlign: 'center', + }, + + item: { + flexDirection: 'row', + alignItems: 'center', + borderBottomWidth: 1, + paddingHorizontal: 20, + paddingVertical: 14, + }, + pr10: { + marginRight: 10, + }, + + btnContainer: { + flexDirection: 'row', + justifyContent: 'center', + }, + btnContainerDesktop: { + marginTop: 14, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + paddingHorizontal: 60, + paddingVertical: 14, + }, + btnLabel: { + fontSize: 18, + }, + + trashIcon: { + color: 'red', + }, +}) diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx index fb44f1f9b..2e162ef0f 100644 --- a/src/view/screens/PostLikedBy.tsx +++ b/src/view/screens/PostLikedBy.tsx @@ -22,7 +22,7 @@ export const PostLikedByScreen = withAuthRequired(({route}: Props) => { return ( <View> - <ViewHeader title="Liked by" /> + <ViewHeader title="Liked by" showOnDesktop /> <PostLikedByComponent uri={uri} /> </View> ) diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index 19f0af18b..bfd827f67 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -22,7 +22,7 @@ export const PostRepostedByScreen = withAuthRequired(({route}: Props) => { return ( <View> - <ViewHeader title="Reposted by" /> + <ViewHeader title="Reposted by" showOnDesktop /> <PostRepostedByComponent uri={uri} /> </View> ) diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index e2f95fbe4..d782cb696 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -20,7 +20,7 @@ export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => { return ( <View> - <ViewHeader title="Followers" /> + <ViewHeader title="Followers" showOnDesktop /> <ProfileFollowersComponent name={name} /> </View> ) diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index f70944f55..9c15d1d92 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -20,7 +20,7 @@ export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => { return ( <View> - <ViewHeader title="Following" /> + <ViewHeader title="Following" showOnDesktop /> <ProfileFollowsComponent name={name} /> </View> ) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 76a3efa60..6cf83c391 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -140,7 +140,7 @@ export const SettingsScreen = withAuthRequired( return ( <View style={[s.hContentRegion]} testID="settingsScreen"> - <ViewHeader title="Settings" /> + <ViewHeader title="Settings" showOnDesktop /> <ScrollView style={s.hContentRegion} scrollIndicatorInsets={{right: 1}}> <View style={styles.spacer20} /> <View style={[s.flexRow, styles.heading]}> @@ -267,6 +267,20 @@ export const SettingsScreen = withAuthRequired( Content moderation </Text> </TouchableOpacity> + <Link + testID="appPasswordBtn" + style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} + href="/settings/app-passwords"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="lock" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + App Passwords + </Text> + </Link> <TouchableOpacity testID="changeHandleBtn" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} |