Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor app passwords to use react-query (#1932)

authored by

Paul Frazee and committed by
GitHub
9f7a162a 310a7eac

+177 -115
-57
src/state/models/me.ts
··· 1 1 import {makeAutoObservable, runInAction} from 'mobx' 2 - import {ComAtprotoServerListAppPasswords} from '@atproto/api' 3 2 import {RootStoreModel} from './root-store' 4 3 import {isObj, hasProp} from 'lib/type-guards' 5 4 import {logger} from '#/logger' ··· 14 13 avatar: string = '' 15 14 followsCount: number | undefined 16 15 followersCount: number | undefined 17 - appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] 18 16 lastProfileStateUpdate = Date.now() 19 17 20 18 constructor(public rootStore: RootStoreModel) { ··· 33 31 this.displayName = '' 34 32 this.description = '' 35 33 this.avatar = '' 36 - this.appPasswords = [] 37 34 } 38 35 39 36 serialize(): unknown { ··· 81 78 this.did = sess.currentSession?.did || '' 82 79 await this.fetchProfile() 83 80 this.rootStore.emitSessionLoaded() 84 - await this.fetchAppPasswords() 85 81 } else { 86 82 this.clear() 87 83 } ··· 92 88 logger.debug('Updating me profile information') 93 89 this.lastProfileStateUpdate = Date.now() 94 90 await this.fetchProfile() 95 - await this.fetchAppPasswords() 96 91 } 97 92 } 98 93 ··· 116 111 this.followersCount = undefined 117 112 } 118 113 }) 119 - } 120 - 121 - async fetchAppPasswords() { 122 - if (this.rootStore.session) { 123 - try { 124 - const res = 125 - await this.rootStore.agent.com.atproto.server.listAppPasswords({}) 126 - runInAction(() => { 127 - this.appPasswords = res.data.passwords 128 - }) 129 - } catch (e) { 130 - logger.error('Failed to fetch user app passwords', { 131 - error: e, 132 - }) 133 - } 134 - } 135 - } 136 - 137 - async createAppPassword(name: string) { 138 - if (this.rootStore.session) { 139 - try { 140 - if (this.appPasswords.find(p => p.name === name)) { 141 - // TODO: this should be handled by the backend but it's not 142 - throw new Error('App password with this name already exists') 143 - } 144 - const res = 145 - await this.rootStore.agent.com.atproto.server.createAppPassword({ 146 - name, 147 - }) 148 - runInAction(() => { 149 - this.appPasswords.push(res.data) 150 - }) 151 - return res.data 152 - } catch (e) { 153 - logger.error('Failed to create app password', {error: e}) 154 - } 155 - } 156 - } 157 - 158 - async deleteAppPassword(name: string) { 159 - if (this.rootStore.session) { 160 - try { 161 - await this.rootStore.agent.com.atproto.server.revokeAppPassword({ 162 - name: name, 163 - }) 164 - runInAction(() => { 165 - this.appPasswords = this.appPasswords.filter(p => p.name !== name) 166 - }) 167 - } catch (e) { 168 - logger.error('Failed to delete app password', {error: e}) 169 - } 170 - } 171 114 } 172 115 }
+56
src/state/queries/app-passwords.ts
··· 1 + import {ComAtprotoServerCreateAppPassword} from '@atproto/api' 2 + import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query' 3 + import {useSession} from '../session' 4 + 5 + export const RQKEY = () => ['app-passwords'] 6 + 7 + export function useAppPasswordsQuery() { 8 + const {agent} = useSession() 9 + return useQuery({ 10 + queryKey: RQKEY(), 11 + queryFn: async () => { 12 + const res = await agent.com.atproto.server.listAppPasswords({}) 13 + return res.data.passwords 14 + }, 15 + }) 16 + } 17 + 18 + export function useAppPasswordCreateMutation() { 19 + const {agent} = useSession() 20 + const queryClient = useQueryClient() 21 + return useMutation< 22 + ComAtprotoServerCreateAppPassword.OutputSchema, 23 + Error, 24 + {name: string} 25 + >({ 26 + mutationFn: async ({name}) => { 27 + return ( 28 + await agent.com.atproto.server.createAppPassword({ 29 + name, 30 + }) 31 + ).data 32 + }, 33 + onSuccess() { 34 + queryClient.invalidateQueries({ 35 + queryKey: RQKEY(), 36 + }) 37 + }, 38 + }) 39 + } 40 + 41 + export function useAppPasswordDeleteMutation() { 42 + const {agent} = useSession() 43 + const queryClient = useQueryClient() 44 + return useMutation<void, Error, {name: string}>({ 45 + mutationFn: async ({name}) => { 46 + await agent.com.atproto.server.revokeAppPassword({ 47 + name, 48 + }) 49 + }, 50 + onSuccess() { 51 + queryClient.invalidateQueries({ 52 + queryKey: RQKEY(), 53 + }) 54 + }, 55 + }) 56 + }
+19 -6
src/view/com/modals/AddAppPasswords.tsx
··· 3 3 import {Text} from '../util/text/Text' 4 4 import {Button} from '../util/forms/Button' 5 5 import {s} from 'lib/styles' 6 - import {useStores} from 'state/index' 7 6 import {usePalette} from 'lib/hooks/usePalette' 8 7 import {isNative} from 'platform/detection' 9 8 import { ··· 16 15 import {Trans, msg} from '@lingui/macro' 17 16 import {useLingui} from '@lingui/react' 18 17 import {useModalControls} from '#/state/modals' 18 + import { 19 + useAppPasswordsQuery, 20 + useAppPasswordCreateMutation, 21 + } from '#/state/queries/app-passwords' 19 22 20 23 export const snapPoints = ['70%'] 21 24 ··· 56 59 57 60 export function Component({}: {}) { 58 61 const pal = usePalette('default') 59 - const store = useStores() 60 62 const {_} = useLingui() 61 63 const {closeModal} = useModalControls() 64 + const {data: passwords} = useAppPasswordsQuery() 65 + const createMutation = useAppPasswordCreateMutation() 62 66 const [name, setName] = useState( 63 67 shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], 64 68 ) ··· 82 86 if (!name || !name.trim()) { 83 87 Toast.show( 84 88 'Please enter a name for your app password. All spaces is not allowed.', 89 + 'times', 85 90 ) 86 91 return 87 92 } 88 93 // if name is too short (under 4 chars), we don't allow it 89 94 if (name.length < 4) { 90 - Toast.show('App Password names must be at least 4 characters long.') 95 + Toast.show( 96 + 'App Password names must be at least 4 characters long.', 97 + 'times', 98 + ) 99 + return 100 + } 101 + 102 + if (passwords?.find(p => p.name === name)) { 103 + Toast.show('This name is already in use', 'times') 91 104 return 92 105 } 93 106 94 107 try { 95 - const newPassword = await store.me.createAppPassword(name) 108 + const newPassword = await createMutation.mutateAsync({name}) 96 109 if (newPassword) { 97 110 setAppPassword(newPassword.password) 98 111 } else { 99 - Toast.show('Failed to create app password.') 112 + Toast.show('Failed to create app password.', 'times') 100 113 // TODO: better error handling (?) 101 114 } 102 115 } catch (e) { 103 - Toast.show('Failed to create app password.') 116 + Toast.show('Failed to create app password.', 'times') 104 117 logger.error('Failed to create app password', {error: e}) 105 118 } 106 119 }
+5 -1
src/view/com/util/Toast.tsx
··· 1 1 import RootSiblings from 'react-native-root-siblings' 2 2 import React from 'react' 3 3 import {Animated, StyleSheet, View} from 'react-native' 4 + import {Props as FontAwesomeProps} from '@fortawesome/react-native-fontawesome' 4 5 import {Text} from './text/Text' 5 6 import {colors} from 'lib/styles' 6 7 import {useTheme} from 'lib/ThemeContext' ··· 9 10 10 11 const TIMEOUT = 4e3 11 12 12 - export function show(message: string) { 13 + export function show( 14 + message: string, 15 + _icon: FontAwesomeProps['icon'] = 'check', 16 + ) { 13 17 const item = new RootSiblings(<Toast message={message} />) 14 18 setTimeout(() => { 15 19 item.destroy()
+6 -3
src/view/com/util/Toast.web.tsx
··· 7 7 import { 8 8 FontAwesomeIcon, 9 9 FontAwesomeIconStyle, 10 + Props as FontAwesomeProps, 10 11 } from '@fortawesome/react-native-fontawesome' 11 12 12 13 const DURATION = 3500 13 14 14 15 interface ActiveToast { 15 16 text: string 17 + icon: FontAwesomeProps['icon'] 16 18 } 17 19 type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void 18 20 ··· 36 38 {activeToast && ( 37 39 <View style={styles.container}> 38 40 <FontAwesomeIcon 39 - icon="check" 41 + icon={activeToast.icon} 40 42 size={24} 41 43 style={styles.icon as FontAwesomeIconStyle} 42 44 /> ··· 49 51 50 52 // methods 51 53 // = 52 - export function show(text: string) { 54 + 55 + export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') { 53 56 if (toastTimeout) { 54 57 clearTimeout(toastTimeout) 55 58 } 56 - globalSetActiveToast?.({text}) 59 + globalSetActiveToast?.({text, icon}) 57 60 toastTimeout = setTimeout(() => { 58 61 globalSetActiveToast?.(undefined) 59 62 }, DURATION)
+91 -48
src/view/screens/AppPasswords.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import { 3 + ActivityIndicator, 4 + StyleSheet, 5 + TouchableOpacity, 6 + View, 7 + } from 'react-native' 3 8 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 9 import {ScrollView} from 'react-native-gesture-handler' 5 10 import {Text} from '../com/util/text/Text' 6 11 import {Button} from '../com/util/forms/Button' 7 12 import * as Toast from '../com/util/Toast' 8 - import {useStores} from 'state/index' 9 13 import {usePalette} from 'lib/hooks/usePalette' 10 14 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 11 15 import {withAuthRequired} from 'view/com/auth/withAuthRequired' 12 - import {observer} from 'mobx-react-lite' 13 16 import {NativeStackScreenProps} from '@react-navigation/native-stack' 14 17 import {CommonNavigatorParams} from 'lib/routes/types' 15 18 import {useAnalytics} from 'lib/analytics/analytics' ··· 21 24 import {useSetMinimalShellMode} from '#/state/shell' 22 25 import {useModalControls} from '#/state/modals' 23 26 import {useLanguagePrefs} from '#/state/preferences' 27 + import { 28 + useAppPasswordsQuery, 29 + useAppPasswordDeleteMutation, 30 + } from '#/state/queries/app-passwords' 31 + import {ErrorScreen} from '../com/util/error/ErrorScreen' 32 + import {cleanError} from '#/lib/strings/errors' 24 33 25 34 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> 26 35 export const AppPasswords = withAuthRequired( 27 - observer(function AppPasswordsImpl({}: Props) { 36 + function AppPasswordsImpl({}: Props) { 28 37 const pal = usePalette('default') 29 - const store = useStores() 30 38 const setMinimalShellMode = useSetMinimalShellMode() 31 39 const {screen} = useAnalytics() 32 40 const {isTabletOrDesktop} = useWebMediaQueries() 33 41 const {openModal} = useModalControls() 42 + const {data: appPasswords, error} = useAppPasswordsQuery() 34 43 35 44 useFocusEffect( 36 45 React.useCallback(() => { ··· 43 52 openModal({name: 'add-app-password'}) 44 53 }, [openModal]) 45 54 55 + if (error) { 56 + return ( 57 + <CenteredView 58 + style={[ 59 + styles.container, 60 + isTabletOrDesktop && styles.containerDesktop, 61 + pal.view, 62 + pal.border, 63 + ]} 64 + testID="appPasswordsScreen"> 65 + <ErrorScreen 66 + title="Oops!" 67 + message="There was an issue with fetching your app passwords" 68 + details={cleanError(error)} 69 + /> 70 + </CenteredView> 71 + ) 72 + } 73 + 46 74 // no app passwords (empty) state 47 - if (store.me.appPasswords.length === 0) { 75 + if (appPasswords?.length === 0) { 48 76 return ( 49 77 <CenteredView 50 78 style={[ ··· 82 110 ) 83 111 } 84 112 85 - // has app passwords 86 - return ( 87 - <CenteredView 88 - style={[ 89 - styles.container, 90 - isTabletOrDesktop && styles.containerDesktop, 91 - pal.view, 92 - pal.border, 93 - ]} 94 - testID="appPasswordsScreen"> 95 - <AppPasswordsHeader /> 96 - <ScrollView 113 + if (appPasswords?.length) { 114 + // has app passwords 115 + return ( 116 + <CenteredView 97 117 style={[ 98 - styles.scrollContainer, 118 + styles.container, 119 + isTabletOrDesktop && styles.containerDesktop, 120 + pal.view, 99 121 pal.border, 100 - !isTabletOrDesktop && styles.flex1, 101 - ]}> 102 - {store.me.appPasswords.map((password, i) => ( 103 - <AppPassword 104 - key={password.name} 105 - testID={`appPassword-${i}`} 106 - name={password.name} 107 - createdAt={password.createdAt} 108 - /> 109 - ))} 110 - {isTabletOrDesktop && ( 111 - <View style={[styles.btnContainer, styles.btnContainerDesktop]}> 122 + ]} 123 + testID="appPasswordsScreen"> 124 + <AppPasswordsHeader /> 125 + <ScrollView 126 + style={[ 127 + styles.scrollContainer, 128 + pal.border, 129 + !isTabletOrDesktop && styles.flex1, 130 + ]}> 131 + {appPasswords.map((password, i) => ( 132 + <AppPassword 133 + key={password.name} 134 + testID={`appPassword-${i}`} 135 + name={password.name} 136 + createdAt={password.createdAt} 137 + /> 138 + ))} 139 + {isTabletOrDesktop && ( 140 + <View style={[styles.btnContainer, styles.btnContainerDesktop]}> 141 + <Button 142 + testID="appPasswordBtn" 143 + type="primary" 144 + label="Add App Password" 145 + style={styles.btn} 146 + labelStyle={styles.btnLabel} 147 + onPress={onAdd} 148 + /> 149 + </View> 150 + )} 151 + </ScrollView> 152 + {!isTabletOrDesktop && ( 153 + <View style={styles.btnContainer}> 112 154 <Button 113 155 testID="appPasswordBtn" 114 156 type="primary" ··· 119 161 /> 120 162 </View> 121 163 )} 122 - </ScrollView> 123 - {!isTabletOrDesktop && ( 124 - <View style={styles.btnContainer}> 125 - <Button 126 - testID="appPasswordBtn" 127 - type="primary" 128 - label="Add App Password" 129 - style={styles.btn} 130 - labelStyle={styles.btnLabel} 131 - onPress={onAdd} 132 - /> 133 - </View> 134 - )} 164 + </CenteredView> 165 + ) 166 + } 167 + 168 + return ( 169 + <CenteredView 170 + style={[ 171 + styles.container, 172 + isTabletOrDesktop && styles.containerDesktop, 173 + pal.view, 174 + pal.border, 175 + ]} 176 + testID="appPasswordsScreen"> 177 + <ActivityIndicator /> 135 178 </CenteredView> 136 179 ) 137 - }), 180 + }, 138 181 ) 139 182 140 183 function AppPasswordsHeader() { ··· 169 212 createdAt: string 170 213 }) { 171 214 const pal = usePalette('default') 172 - const store = useStores() 173 215 const {_} = useLingui() 174 216 const {openModal} = useModalControls() 175 217 const {contentLanguages} = useLanguagePrefs() 218 + const deleteMutation = useAppPasswordDeleteMutation() 176 219 177 220 const onDelete = React.useCallback(async () => { 178 221 openModal({ ··· 180 223 title: 'Delete App Password', 181 224 message: `Are you sure you want to delete the app password "${name}"?`, 182 225 async onPressConfirm() { 183 - await store.me.deleteAppPassword(name) 226 + await deleteMutation.mutateAsync({name}) 184 227 Toast.show('App password deleted') 185 228 }, 186 229 }) 187 - }, [store, openModal, name]) 230 + }, [deleteMutation, openModal, name]) 188 231 189 232 const primaryLocale = 190 233 contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'