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