forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback} from 'react'
2import {View} from 'react-native'
3import Animated, {
4 FadeIn,
5 FadeOut,
6 LayoutAnimationConfig,
7 LinearTransition,
8} from 'react-native-reanimated'
9import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12import {type NativeStackScreenProps} from '@react-navigation/native-stack'
13
14import {type CommonNavigatorParams} from '#/lib/routes/types'
15import {cleanError} from '#/lib/strings/errors'
16import {
17 useAppPasswordDeleteMutation,
18 useAppPasswordsQuery,
19} from '#/state/queries/app-passwords'
20import {EmptyState} from '#/view/com/util/EmptyState'
21import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
22import * as Toast from '#/view/com/util/Toast'
23import {atoms as a, useTheme} from '#/alf'
24import {Admonition, colors} from '#/components/Admonition'
25import {Button, ButtonIcon, ButtonText} from '#/components/Button'
26import {useDialogControl} from '#/components/Dialog'
27import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
28import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
29import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
30import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
31import * as Layout from '#/components/Layout'
32import {Loader} from '#/components/Loader'
33import * as Prompt from '#/components/Prompt'
34import {Text} from '#/components/Typography'
35import {AddAppPasswordDialog} from './components/AddAppPasswordDialog'
36import * as SettingsList from './components/SettingsList'
37
38type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
39export function AppPasswordsScreen({}: Props) {
40 const {_} = useLingui()
41 const {data: appPasswords, error} = useAppPasswordsQuery()
42 const createAppPasswordControl = useDialogControl()
43
44 return (
45 <Layout.Screen testID="AppPasswordsScreen">
46 <Layout.Header.Outer>
47 <Layout.Header.BackButton />
48 <Layout.Header.Content>
49 <Layout.Header.TitleText>
50 <Trans>App Passwords</Trans>
51 </Layout.Header.TitleText>
52 </Layout.Header.Content>
53 <Layout.Header.Slot />
54 </Layout.Header.Outer>
55 <Layout.Content>
56 {error ? (
57 <ErrorScreen
58 title={_(msg`Oops!`)}
59 message={_(msg`There was an issue fetching your app passwords`)}
60 details={cleanError(error)}
61 />
62 ) : (
63 <SettingsList.Container>
64 <SettingsList.Item>
65 <Admonition type="tip" style={[a.flex_1]}>
66 <Trans>
67 Use app passwords to sign in to other Bluesky clients without
68 giving full access to your account or password.
69 </Trans>
70 </Admonition>
71 </SettingsList.Item>
72 <SettingsList.Item>
73 <Button
74 label={_(msg`Add App Password`)}
75 size="large"
76 color="primary"
77 variant="solid"
78 onPress={() => createAppPasswordControl.open()}
79 style={[a.flex_1]}>
80 <ButtonIcon icon={PlusIcon} />
81 <ButtonText>
82 <Trans>Add App Password</Trans>
83 </ButtonText>
84 </Button>
85 </SettingsList.Item>
86 <SettingsList.Divider />
87 <LayoutAnimationConfig skipEntering skipExiting>
88 {appPasswords ? (
89 appPasswords.length > 0 ? (
90 <View style={[a.overflow_hidden]}>
91 {appPasswords.map(appPassword => (
92 <Animated.View
93 key={appPassword.name}
94 style={a.w_full}
95 entering={FadeIn}
96 exiting={FadeOut}
97 layout={LinearTransition.delay(150)}>
98 <SettingsList.Item>
99 <AppPasswordCard appPassword={appPassword} />
100 </SettingsList.Item>
101 </Animated.View>
102 ))}
103 </View>
104 ) : (
105 <EmptyState
106 icon={Growth}
107 message={_(msg`No app passwords yet`)}
108 />
109 )
110 ) : (
111 <View
112 style={[
113 a.flex_1,
114 a.justify_center,
115 a.align_center,
116 a.py_4xl,
117 ]}>
118 <Loader size="xl" />
119 </View>
120 )}
121 </LayoutAnimationConfig>
122 </SettingsList.Container>
123 )}
124 </Layout.Content>
125
126 <AddAppPasswordDialog
127 control={createAppPasswordControl}
128 passwords={appPasswords?.map(p => p.name) || []}
129 />
130 </Layout.Screen>
131 )
132}
133
134function AppPasswordCard({
135 appPassword,
136}: {
137 appPassword: ComAtprotoServerListAppPasswords.AppPassword
138}) {
139 const t = useTheme()
140 const {i18n, _} = useLingui()
141 const deleteControl = Prompt.usePromptControl()
142 const {mutateAsync: deleteMutation} = useAppPasswordDeleteMutation()
143
144 const onDelete = useCallback(async () => {
145 await deleteMutation({name: appPassword.name})
146 Toast.show(_(msg({message: 'App password deleted', context: 'toast'})))
147 }, [deleteMutation, appPassword.name, _])
148
149 return (
150 <View
151 style={[
152 a.w_full,
153 a.border,
154 a.rounded_sm,
155 a.px_md,
156 a.py_sm,
157 t.atoms.bg_contrast_25,
158 t.atoms.border_contrast_low,
159 ]}>
160 <View
161 style={[
162 a.flex_row,
163 a.justify_between,
164 a.align_start,
165 a.w_full,
166 a.gap_sm,
167 ]}>
168 <View style={[a.gap_xs]}>
169 <Text style={[t.atoms.text, a.text_md, a.font_semi_bold]}>
170 {appPassword.name}
171 </Text>
172 <Text style={[t.atoms.text_contrast_medium]}>
173 <Trans>
174 Created{' '}
175 {i18n.date(appPassword.createdAt, {
176 year: 'numeric',
177 month: 'numeric',
178 day: 'numeric',
179 hour: '2-digit',
180 minute: '2-digit',
181 })}
182 </Trans>
183 </Text>
184 </View>
185 <Button
186 label={_(msg`Delete app password`)}
187 variant="ghost"
188 color="negative"
189 size="small"
190 shape="square"
191 style={[a.bg_transparent]}
192 onPress={() => deleteControl.open()}>
193 <ButtonIcon icon={TrashIcon} />
194 </Button>
195 </View>
196 {appPassword.privileged && (
197 <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
198 <WarningIcon style={[{color: colors.warning}]} />
199 <Text style={t.atoms.text_contrast_high}>
200 <Trans>Allows access to direct messages</Trans>
201 </Text>
202 </View>
203 )}
204
205 <Prompt.Basic
206 control={deleteControl}
207 title={_(msg`Delete app password?`)}
208 description={_(
209 msg`Are you sure you want to delete the app password "${appPassword.name}"?`,
210 )}
211 onConfirm={onDelete}
212 confirmButtonCta={_(msg`Delete`)}
213 confirmButtonColor="negative"
214 />
215 </View>
216 )
217}