Bluesky app fork with some witchin' additions 馃挮
at main 300 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, 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 {useTheme} from '#/lib/ThemeContext' 18import {isAndroid, isMobileWeb, isWeb} from '#/platform/detection' 19import {useModalControls} from '#/state/modals' 20import { 21 getMembership, 22 type ListMembersip, 23 useDangerousListMembershipsQuery, 24 useListMembershipAddMutation, 25 useListMembershipRemoveMutation, 26} from '#/state/queries/list-memberships' 27import {useSession} from '#/state/session' 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 (isMobileWeb) { 61 return [pal.border, {height: screenHeight / 2}] 62 } else if (isWeb) { 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 t = useTheme(); 142 const pal = usePalette('default') 143 const {_} = useLingui() 144 const {currentAccount} = useSession() 145 const [isProcessing, setIsProcessing] = React.useState(false) 146 const membership = React.useMemo( 147 () => getMembership(memberships, list.uri, subject), 148 [memberships, list.uri, subject], 149 ) 150 const listMembershipAddMutation = useListMembershipAddMutation() 151 const listMembershipRemoveMutation = useListMembershipRemoveMutation() 152 153 const onToggleMembership = useCallback(async () => { 154 if (typeof membership === 'undefined') { 155 return 156 } 157 setIsProcessing(true) 158 try { 159 if (membership === false) { 160 await listMembershipAddMutation.mutateAsync({ 161 listUri: list.uri, 162 actorDid: subject, 163 }) 164 Toast.show(_(msg`Added to list`)) 165 onAdd?.(list.uri) 166 } else { 167 await listMembershipRemoveMutation.mutateAsync({ 168 listUri: list.uri, 169 actorDid: subject, 170 membershipUri: membership, 171 }) 172 Toast.show(_(msg`Removed from list`)) 173 onRemove?.(list.uri) 174 } 175 } catch (e) { 176 Toast.show(cleanError(e), 'xmark') 177 } finally { 178 setIsProcessing(false) 179 } 180 }, [ 181 _, 182 list, 183 subject, 184 membership, 185 setIsProcessing, 186 onAdd, 187 onRemove, 188 listMembershipAddMutation, 189 listMembershipRemoveMutation, 190 ]) 191 192 return ( 193 <View 194 testID={`toggleBtn-${list.name}`} 195 style={[ 196 styles.listItem, 197 pal.border, 198 index !== 0 && {borderTopWidth: StyleSheet.hairlineWidth}, 199 ]}> 200 <View style={styles.listItemAvi}> 201 <UserAvatar size={40} avatar={list.avatar} type="list" /> 202 </View> 203 <View style={styles.listItemContent}> 204 <Text 205 type="lg" 206 style={[s.bold, pal.text]} 207 numberOfLines={1} 208 lineHeight={1.2}> 209 {sanitizeDisplayName(list.name)} 210 </Text> 211 <Text type="md" style={[pal.textLight]} numberOfLines={1}> 212 {list.purpose === 'app.bsky.graph.defs#curatelist' && 213 (list.creator.did === currentAccount?.did ? ( 214 <Trans>User list by you</Trans> 215 ) : ( 216 <Trans> 217 User list by {sanitizeHandle(list.creator.handle, '@')} 218 </Trans> 219 ))} 220 {list.purpose === 'app.bsky.graph.defs#modlist' && 221 (list.creator.did === currentAccount?.did ? ( 222 <Trans>Moderation list by you</Trans> 223 ) : ( 224 <Trans> 225 Moderation list by {sanitizeHandle(list.creator.handle, '@')} 226 </Trans> 227 ))} 228 </Text> 229 </View> 230 <View> 231 {isProcessing || typeof membership === 'undefined' ? ( 232 <ActivityIndicator color={t.palette.primary_500} /> 233 ) : ( 234 <Button 235 testID={`user-${handle}-addBtn`} 236 type="default" 237 label={membership === false ? _(msg`Add`) : _(msg`Remove`)} 238 onPress={onToggleMembership} 239 /> 240 )} 241 </View> 242 </View> 243 ) 244} 245 246const styles = StyleSheet.create({ 247 container: { 248 paddingHorizontal: isWeb ? 0 : 16, 249 }, 250 btns: { 251 position: 'relative', 252 flexDirection: 'row', 253 alignItems: 'center', 254 justifyContent: 'center', 255 gap: 10, 256 paddingTop: 10, 257 paddingBottom: isAndroid ? 10 : 0, 258 borderTopWidth: StyleSheet.hairlineWidth, 259 }, 260 footerBtn: { 261 paddingHorizontal: 24, 262 paddingVertical: 12, 263 }, 264 265 listItem: { 266 flexDirection: 'row', 267 alignItems: 'center', 268 paddingHorizontal: 14, 269 paddingVertical: 10, 270 }, 271 listItemAvi: { 272 width: 54, 273 paddingLeft: 4, 274 paddingTop: 8, 275 paddingBottom: 10, 276 }, 277 listItemContent: { 278 flex: 1, 279 paddingRight: 10, 280 paddingTop: 10, 281 paddingBottom: 10, 282 }, 283 checkbox: { 284 flexDirection: 'row', 285 alignItems: 'center', 286 justifyContent: 'center', 287 borderWidth: 1, 288 width: 24, 289 height: 24, 290 borderRadius: 6, 291 marginRight: 8, 292 }, 293 loadingContainer: { 294 position: 'absolute', 295 top: 10, 296 right: 0, 297 bottom: 0, 298 justifyContent: 'center', 299 }, 300})