Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 301 lines 7.9 kB view raw
1import {useCallback, useMemo, useState} 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 = 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] = useState(false) 147 const membership = 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={[{fontWeight: '600'}, 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})