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