Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {View} from 'react-native'
2import {
3 type $Typed,
4 type AppBskyGraphDefs,
5 type AppBskyGraphListitem,
6 type AppBskyGraphStarterpack,
7 AtUri,
8 type ComAtprotoRepoApplyWrites,
9} from '@atproto/api'
10import {TID} from '@atproto/common-web'
11import {msg} from '@lingui/core/macro'
12import {useLingui} from '@lingui/react'
13import {Trans} from '@lingui/react/macro'
14import {useNavigation} from '@react-navigation/native'
15import {useQueryClient} from '@tanstack/react-query'
16import chunk from 'lodash.chunk'
17
18import {until} from '#/lib/async/until'
19import {wait} from '#/lib/async/wait'
20import {type NavigationProp} from '#/lib/routes/types'
21import {logger} from '#/logger'
22import {getAllListMembers} from '#/state/queries/list-members'
23import {useAgent, useSession} from '#/state/session'
24import {atoms as a, platform, useTheme, web} from '#/alf'
25import {Admonition} from '#/components/Admonition'
26import {Button, ButtonText} from '#/components/Button'
27import * as Dialog from '#/components/Dialog'
28import {Loader} from '#/components/Loader'
29import * as Toast from '#/components/Toast'
30import {Text} from '#/components/Typography'
31import {useAnalytics} from '#/analytics'
32import {CreateOrEditListDialog} from './CreateOrEditListDialog'
33
34export function CreateListFromStarterPackDialog({
35 control,
36 starterPack,
37}: {
38 control: Dialog.DialogControlProps
39 starterPack: AppBskyGraphDefs.StarterPackView
40}) {
41 const {_} = useLingui()
42 const t = useTheme()
43 const agent = useAgent()
44 const ax = useAnalytics()
45 const {currentAccount} = useSession()
46 const navigation = useNavigation<NavigationProp>()
47 const queryClient = useQueryClient()
48 const createDialogControl = Dialog.useDialogControl()
49 const loadingDialogControl = Dialog.useDialogControl()
50
51 const record = starterPack.record as AppBskyGraphStarterpack.Record
52
53 const onPressCreate = () => {
54 control.close(() => createDialogControl.open())
55 }
56
57 const addMembersAndNavigate = async (listUri: string) => {
58 const navigateToList = () => {
59 const urip = new AtUri(listUri)
60 navigation.navigate('ProfileList', {
61 name: urip.hostname,
62 rkey: urip.rkey,
63 })
64 }
65
66 if (!starterPack.list || !currentAccount) {
67 loadingDialogControl.close(navigateToList)
68 return
69 }
70
71 try {
72 // Fetch all members and add them, with minimum 3s duration for UX
73 const listItems = await wait(
74 3000,
75 (async () => {
76 const items = await getAllListMembers(agent, starterPack.list!.uri)
77
78 if (items.length > 0) {
79 const listitemWrites: $Typed<ComAtprotoRepoApplyWrites.Create>[] =
80 items.map(item => {
81 const listitemRecord: $Typed<AppBskyGraphListitem.Record> = {
82 $type: 'app.bsky.graph.listitem',
83 subject: item.subject.did,
84 list: listUri,
85 createdAt: new Date().toISOString(),
86 }
87 return {
88 $type: 'com.atproto.repo.applyWrites#create',
89 collection: 'app.bsky.graph.listitem',
90 rkey: TID.nextStr(),
91 value: listitemRecord,
92 }
93 })
94
95 const chunks = chunk(listitemWrites, 50)
96 for (const c of chunks) {
97 await agent.com.atproto.repo.applyWrites({
98 repo: currentAccount.did,
99 writes: c,
100 })
101 }
102
103 await until(
104 5,
105 1e3,
106 (res: {data: {items: unknown[]}}) => res.data.items.length > 0,
107 () =>
108 agent.app.bsky.graph.getList({
109 list: listUri,
110 limit: 1,
111 }),
112 )
113 }
114
115 return items
116 })(),
117 )
118
119 queryClient.invalidateQueries({queryKey: ['list-members', listUri]})
120
121 ax.metric('starterPack:convertToList', {
122 starterPack: starterPack.uri,
123 memberCount: listItems.length,
124 })
125 } catch (e) {
126 logger.error('Failed to add members to list', {safeMessage: e})
127 Toast.show(_(msg`List created, but failed to add some members`), {
128 type: 'error',
129 })
130 }
131
132 loadingDialogControl.close(navigateToList)
133 }
134
135 const onListCreated = (listUri: string) => {
136 loadingDialogControl.open()
137 addMembersAndNavigate(listUri)
138 }
139
140 return (
141 <>
142 <Dialog.Outer
143 control={control}
144 testID="createListFromStarterPackDialog"
145 nativeOptions={{preventExpansion: true}}>
146 <Dialog.Handle />
147 <Dialog.ScrollableInner
148 label={_(msg`Create list from starter pack`)}
149 style={web({maxWidth: 400})}>
150 <View style={[a.gap_lg]}>
151 <Text style={[a.text_xl, a.font_bold]}>
152 <Trans>Create list from starter pack</Trans>
153 </Text>
154
155 <Text
156 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
157 <Trans>
158 This will create a new list with the same name, description, and
159 members as this starter pack.
160 </Trans>
161 </Text>
162
163 <Admonition type="tip">
164 <Trans>
165 Changes to the starter pack will not be reflected in the list
166 after creation. The list will be an independent copy.
167 </Trans>
168 </Admonition>
169
170 <View
171 style={[
172 platform({
173 web: [a.flex_row_reverse],
174 native: [a.flex_col],
175 }),
176 a.gap_md,
177 a.pt_sm,
178 ]}>
179 <Button
180 label={_(msg`Create list`)}
181 onPress={onPressCreate}
182 size={platform({
183 web: 'small',
184 native: 'large',
185 })}
186 color="primary">
187 <ButtonText>
188 <Trans>Create list</Trans>
189 </ButtonText>
190 </Button>
191 <Button
192 label={_(msg`Cancel`)}
193 onPress={() => control.close()}
194 size={platform({
195 web: 'small',
196 native: 'large',
197 })}
198 color="secondary">
199 <ButtonText>
200 <Trans>Cancel</Trans>
201 </ButtonText>
202 </Button>
203 </View>
204 </View>
205 <Dialog.Close />
206 </Dialog.ScrollableInner>
207 </Dialog.Outer>
208
209 <CreateOrEditListDialog
210 control={createDialogControl}
211 purpose="app.bsky.graph.defs#curatelist"
212 onSave={onListCreated}
213 initialValues={{
214 name: record.name,
215 description: record.description,
216 avatar: starterPack.list?.avatar,
217 }}
218 />
219
220 <Dialog.Outer
221 control={loadingDialogControl}
222 nativeOptions={{preventDismiss: true}}>
223 <Dialog.Handle />
224 <Dialog.ScrollableInner
225 label={_(msg`Adding members to list...`)}
226 style={web({maxWidth: 400})}>
227 <View style={[a.align_center, a.gap_lg, a.py_5xl]}>
228 <Loader size="xl" />
229 <Text style={[a.text_lg, t.atoms.text_contrast_high]}>
230 <Trans>Adding members to list...</Trans>
231 </Text>
232 </View>
233 </Dialog.ScrollableInner>
234 </Dialog.Outer>
235 </>
236 )
237}