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