mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useCallback} from 'react'
2import {
3 ActivityIndicator,
4 StyleSheet,
5 useWindowDimensions,
6 View,
7} from 'react-native'
8import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
9import {msg, Trans} from '@lingui/macro'
10import {useLingui} from '@lingui/react'
11
12import {cleanError} from '#/lib/strings/errors'
13import {useModalControls} from '#/state/modals'
14import {
15 getMembership,
16 ListMembersip,
17 useDangerousListMembershipsQuery,
18 useListMembershipAddMutation,
19 useListMembershipRemoveMutation,
20} from '#/state/queries/list-memberships'
21import {useSession} from '#/state/session'
22import {usePalette} from 'lib/hooks/usePalette'
23import {sanitizeDisplayName} from 'lib/strings/display-names'
24import {sanitizeHandle} from 'lib/strings/handles'
25import {s} from 'lib/styles'
26import {isAndroid, isMobileWeb, isWeb} from 'platform/detection'
27import {MyLists} from '../lists/MyLists'
28import {Button} from '../util/forms/Button'
29import {Text} from '../util/text/Text'
30import * as Toast from '../util/Toast'
31import {UserAvatar} from '../util/UserAvatar'
32
33export const snapPoints = ['fullscreen']
34
35export function Component({
36 subject,
37 handle,
38 displayName,
39 onAdd,
40 onRemove,
41}: {
42 subject: string
43 handle: string
44 displayName: string
45 onAdd?: (listUri: string) => void
46 onRemove?: (listUri: string) => void
47}) {
48 const {closeModal} = useModalControls()
49 const pal = usePalette('default')
50 const {height: screenHeight} = useWindowDimensions()
51 const {_} = useLingui()
52 const {data: memberships} = useDangerousListMembershipsQuery()
53
54 const onPressDone = useCallback(() => {
55 closeModal()
56 }, [closeModal])
57
58 const listStyle = React.useMemo(() => {
59 if (isMobileWeb) {
60 return [pal.border, {height: screenHeight / 2}]
61 } else if (isWeb) {
62 return [pal.border, {height: screenHeight / 1.5}]
63 }
64
65 return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
66 }, [pal.border, screenHeight])
67
68 return (
69 <View testID="userAddRemoveListsModal" style={s.hContentRegion}>
70 <Text
71 style={[
72 {
73 textAlign: 'center',
74 fontWeight: 'bold',
75 fontSize: 20,
76 marginBottom: 12,
77 paddingHorizontal: 12,
78 },
79 pal.text,
80 ]}
81 numberOfLines={1}>
82 <Trans>Update {displayName} in Lists</Trans>
83 </Text>
84 <MyLists
85 filter="all"
86 inline
87 renderItem={(list, index) => (
88 <ListItem
89 key={list.uri}
90 index={index}
91 list={list}
92 memberships={memberships}
93 subject={subject}
94 handle={handle}
95 onAdd={onAdd}
96 onRemove={onRemove}
97 />
98 )}
99 style={listStyle}
100 />
101 <View style={[styles.btns, pal.border]}>
102 <Button
103 testID="doneBtn"
104 type="default"
105 onPress={onPressDone}
106 style={styles.footerBtn}
107 accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
108 accessibilityHint=""
109 onAccessibilityEscape={onPressDone}
110 label={_(msg({message: `Done`, context: 'action'}))}
111 />
112 </View>
113 </View>
114 )
115}
116
117function ListItem({
118 index,
119 list,
120 memberships,
121 subject,
122 handle,
123 onAdd,
124 onRemove,
125}: {
126 index: number
127 list: GraphDefs.ListView
128 memberships: ListMembersip[] | undefined
129 subject: string
130 handle: string
131 onAdd?: (listUri: string) => void
132 onRemove?: (listUri: string) => void
133}) {
134 const pal = usePalette('default')
135 const {_} = useLingui()
136 const {currentAccount} = useSession()
137 const [isProcessing, setIsProcessing] = React.useState(false)
138 const membership = React.useMemo(
139 () => getMembership(memberships, list.uri, subject),
140 [memberships, list.uri, subject],
141 )
142 const listMembershipAddMutation = useListMembershipAddMutation()
143 const listMembershipRemoveMutation = useListMembershipRemoveMutation()
144
145 const onToggleMembership = useCallback(async () => {
146 if (typeof membership === 'undefined') {
147 return
148 }
149 setIsProcessing(true)
150 try {
151 if (membership === false) {
152 await listMembershipAddMutation.mutateAsync({
153 listUri: list.uri,
154 actorDid: subject,
155 })
156 Toast.show(_(msg`Added to list`))
157 onAdd?.(list.uri)
158 } else {
159 await listMembershipRemoveMutation.mutateAsync({
160 listUri: list.uri,
161 actorDid: subject,
162 membershipUri: membership,
163 })
164 Toast.show(_(msg`Removed from list`))
165 onRemove?.(list.uri)
166 }
167 } catch (e) {
168 Toast.show(cleanError(e), 'xmark')
169 } finally {
170 setIsProcessing(false)
171 }
172 }, [
173 _,
174 list,
175 subject,
176 membership,
177 setIsProcessing,
178 onAdd,
179 onRemove,
180 listMembershipAddMutation,
181 listMembershipRemoveMutation,
182 ])
183
184 return (
185 <View
186 testID={`toggleBtn-${list.name}`}
187 style={[
188 styles.listItem,
189 pal.border,
190 index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth},
191 ]}>
192 <View style={styles.listItemAvi}>
193 <UserAvatar size={40} avatar={list.avatar} type="list" />
194 </View>
195 <View style={styles.listItemContent}>
196 <Text
197 type="lg"
198 style={[s.bold, pal.text]}
199 numberOfLines={1}
200 lineHeight={1.2}>
201 {sanitizeDisplayName(list.name)}
202 </Text>
203 <Text type="md" style={[pal.textLight]} numberOfLines={1}>
204 {list.purpose === 'app.bsky.graph.defs#curatelist' &&
205 (list.creator.did === currentAccount?.did ? (
206 <Trans>User list by you</Trans>
207 ) : (
208 <Trans>
209 User list by {sanitizeHandle(list.creator.handle, '@')}
210 </Trans>
211 ))}
212 {list.purpose === 'app.bsky.graph.defs#modlist' &&
213 (list.creator.did === currentAccount?.did ? (
214 <Trans>Moderation list by you</Trans>
215 ) : (
216 <Trans>
217 Moderation list by {sanitizeHandle(list.creator.handle, '@')}
218 </Trans>
219 ))}
220 </Text>
221 </View>
222 <View>
223 {isProcessing || typeof membership === 'undefined' ? (
224 <ActivityIndicator />
225 ) : (
226 <Button
227 testID={`user-${handle}-addBtn`}
228 type="default"
229 label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
230 onPress={onToggleMembership}
231 />
232 )}
233 </View>
234 </View>
235 )
236}
237
238const styles = StyleSheet.create({
239 container: {
240 paddingHorizontal: isWeb ? 0 : 16,
241 },
242 btns: {
243 position: 'relative',
244 flexDirection: 'row',
245 alignItems: 'center',
246 justifyContent: 'center',
247 gap: 10,
248 paddingTop: 10,
249 paddingBottom: isAndroid ? 10 : 0,
250 borderTopWidth: StyleSheet.hairlineWidth,
251 },
252 footerBtn: {
253 paddingHorizontal: 24,
254 paddingVertical: 12,
255 },
256
257 listItem: {
258 flexDirection: 'row',
259 alignItems: 'center',
260 paddingHorizontal: 14,
261 paddingVertical: 10,
262 },
263 listItemAvi: {
264 width: 54,
265 paddingLeft: 4,
266 paddingTop: 8,
267 paddingBottom: 10,
268 },
269 listItemContent: {
270 flex: 1,
271 paddingRight: 10,
272 paddingTop: 10,
273 paddingBottom: 10,
274 },
275 checkbox: {
276 flexDirection: 'row',
277 alignItems: 'center',
278 justifyContent: 'center',
279 borderWidth: 1,
280 width: 24,
281 height: 24,
282 borderRadius: 6,
283 marginRight: 8,
284 },
285 loadingContainer: {
286 position: 'absolute',
287 top: 10,
288 right: 0,
289 bottom: 0,
290 justifyContent: 'center',
291 },
292})