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