mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at thread-bug 7.7 kB view raw
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})