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