my fork of the bluesky client

Add `list hidden` screen (#4958)

Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>

authored by hailey.at hailey.at

Eric Bailey and committed by
GitHub
723896a4 e54298ec

+494 -339
+4 -28
src/components/Error.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 - import {useNavigation} from '@react-navigation/core' 6 - import {StackActions} from '@react-navigation/native' 7 5 8 - import {NavigationProp} from 'lib/routes/types' 6 + import {useGoBack} from 'lib/hooks/useGoBack' 9 7 import {CenteredView} from 'view/com/util/Views' 10 8 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 9 import {Button, ButtonText} from '#/components/Button' 12 10 import {Text} from '#/components/Typography' 13 - import {router} from '#/routes' 14 11 15 12 export function Error({ 16 13 title, 17 14 message, 18 15 onRetry, 19 - onGoBack: onGoBackProp, 16 + onGoBack, 20 17 hideBackButton, 21 18 sideBorders = true, 22 19 }: { ··· 27 24 hideBackButton?: boolean 28 25 sideBorders?: boolean 29 26 }) { 30 - const navigation = useNavigation<NavigationProp>() 31 27 const {_} = useLingui() 32 28 const t = useTheme() 33 29 const {gtMobile} = useBreakpoints() 34 - 35 - const canGoBack = navigation.canGoBack() 36 - const onGoBack = React.useCallback(() => { 37 - if (onGoBackProp) { 38 - onGoBackProp() 39 - return 40 - } 41 - if (canGoBack) { 42 - navigation.goBack() 43 - } else { 44 - navigation.navigate('HomeTab') 45 - 46 - // Checking the state for routes ensures that web doesn't encounter errors while going back 47 - if (navigation.getState()?.routes) { 48 - navigation.dispatch(StackActions.push(...router.matchPath('/'))) 49 - } else { 50 - navigation.navigate('HomeTab') 51 - navigation.dispatch(StackActions.popToTop()) 52 - } 53 - } 54 - }, [navigation, canGoBack, onGoBackProp]) 30 + const goBack = useGoBack(onGoBack) 55 31 56 32 return ( 57 33 <CenteredView ··· 96 72 variant="solid" 97 73 color={onRetry ? 'secondary' : 'primary'} 98 74 label={_(msg`Return to previous page`)} 99 - onPress={onGoBack} 75 + onPress={goBack} 100 76 size="large" 101 77 style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}> 102 78 <ButtonText>
+42 -6
src/components/ListCard.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import {AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' 3 + import { 4 + AppBskyActorDefs, 5 + AppBskyGraphDefs, 6 + AtUri, 7 + moderateUserList, 8 + ModerationUI, 9 + } from '@atproto/api' 4 10 import {Trans} from '@lingui/macro' 5 11 import {useQueryClient} from '@tanstack/react-query' 6 12 7 13 import {sanitizeHandle} from 'lib/strings/handles' 14 + import {useModerationOpts} from 'state/preferences/moderation-opts' 8 15 import {precacheList} from 'state/queries/feed' 9 - import {useTheme} from '#/alf' 10 - import {atoms as a} from '#/alf' 16 + import {useSession} from 'state/session' 17 + import {atoms as a, useTheme} from '#/alf' 11 18 import { 12 19 Avatar, 13 20 Description, ··· 16 23 SaveButton, 17 24 } from '#/components/FeedCard' 18 25 import {Link as InternalLink, LinkProps} from '#/components/Link' 26 + import * as Hider from '#/components/moderation/Hider' 19 27 import {Text} from '#/components/Typography' 20 28 21 29 /* ··· 43 51 44 52 export function Default(props: Props) { 45 53 const {view, showPinButton} = props 54 + const moderationOpts = useModerationOpts() 55 + const moderation = moderationOpts 56 + ? moderateUserList(view, moderationOpts) 57 + : undefined 58 + 46 59 return ( 47 60 <Link {...props}> 48 61 <Outer> ··· 52 65 title={view.name} 53 66 creator={view.creator} 54 67 purpose={view.purpose} 68 + modUi={moderation?.ui('contentView')} 55 69 /> 56 70 {showPinButton && view.purpose === CURATELIST && ( 57 71 <SaveButton view={view} pin /> ··· 89 103 title, 90 104 creator, 91 105 purpose = CURATELIST, 106 + modUi, 92 107 }: { 93 108 title: string 94 109 creator?: AppBskyActorDefs.ProfileViewBasic 95 110 purpose?: AppBskyGraphDefs.ListView['purpose'] 111 + modUi?: ModerationUI 96 112 }) { 97 113 const t = useTheme() 114 + const {currentAccount} = useSession() 98 115 99 116 return ( 100 117 <View style={[a.flex_1]}> 101 - <Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> 102 - {title} 103 - </Text> 118 + <Hider.Outer 119 + modui={modUi} 120 + isContentVisibleInitialState={ 121 + creator && currentAccount?.did === creator.did 122 + } 123 + allowOverride={creator && currentAccount?.did === creator.did}> 124 + <Hider.Mask> 125 + <Text 126 + style={[a.text_md, a.font_bold, a.leading_snug, a.italic]} 127 + numberOfLines={1}> 128 + <Trans>Hidden list</Trans> 129 + </Text> 130 + </Hider.Mask> 131 + <Hider.Content> 132 + <Text 133 + style={[a.text_md, a.font_bold, a.leading_snug]} 134 + numberOfLines={1}> 135 + {title} 136 + </Text> 137 + </Hider.Content> 138 + </Hider.Outer> 139 + 104 140 {creator && ( 105 141 <Text 106 142 style={[a.leading_snug, t.atoms.text_contrast_medium]}
+89
src/components/moderation/Hider.tsx
··· 1 + import React from 'react' 2 + import {ModerationUI} from '@atproto/api' 3 + 4 + import { 5 + ModerationCauseDescription, 6 + useModerationCauseDescription, 7 + } from '#/lib/moderation/useModerationCauseDescription' 8 + import { 9 + ModerationDetailsDialog, 10 + useModerationDetailsDialogControl, 11 + } from '#/components/moderation/ModerationDetailsDialog' 12 + 13 + type Context = { 14 + isContentVisible: boolean 15 + setIsContentVisible: (show: boolean) => void 16 + info: ModerationCauseDescription 17 + showInfoDialog: () => void 18 + meta: { 19 + isNoPwi: boolean 20 + allowOverride: boolean 21 + } 22 + } 23 + 24 + const Context = React.createContext<Context>({} as Context) 25 + 26 + export const useHider = () => React.useContext(Context) 27 + 28 + export function Outer({ 29 + modui, 30 + isContentVisibleInitialState, 31 + allowOverride, 32 + children, 33 + }: React.PropsWithChildren<{ 34 + isContentVisibleInitialState?: boolean 35 + allowOverride?: boolean 36 + modui: ModerationUI | undefined 37 + }>) { 38 + const control = useModerationDetailsDialogControl() 39 + const blur = modui?.blurs[0] 40 + const [isContentVisible, setIsContentVisible] = React.useState( 41 + isContentVisibleInitialState || !blur, 42 + ) 43 + const info = useModerationCauseDescription(blur) 44 + 45 + const meta = { 46 + isNoPwi: Boolean( 47 + modui?.blurs.find( 48 + cause => 49 + cause.type === 'label' && 50 + cause.labelDef.identifier === '!no-unauthenticated', 51 + ), 52 + ), 53 + allowOverride: allowOverride ?? !modui?.noOverride, 54 + } 55 + 56 + const showInfoDialog = () => { 57 + control.open() 58 + } 59 + 60 + const onSetContentVisible = (show: boolean) => { 61 + if (meta.allowOverride) return 62 + setIsContentVisible(show) 63 + } 64 + 65 + const ctx = { 66 + isContentVisible, 67 + setIsContentVisible: onSetContentVisible, 68 + showInfoDialog, 69 + info, 70 + meta, 71 + } 72 + 73 + return ( 74 + <Context.Provider value={ctx}> 75 + {children} 76 + <ModerationDetailsDialog control={control} modcause={blur} /> 77 + </Context.Provider> 78 + ) 79 + } 80 + 81 + export function Content({children}: {children: React.ReactNode}) { 82 + const ctx = useHider() 83 + return ctx.isContentVisible ? children : null 84 + } 85 + 86 + export function Mask({children}: {children: React.ReactNode}) { 87 + const ctx = useHider() 88 + return ctx.isContentVisible ? null : children 89 + }
+2 -2
src/components/moderation/ModerationDetailsDialog.tsx
··· 18 18 19 19 export interface ModerationDetailsDialogProps { 20 20 control: Dialog.DialogOuterProps['control'] 21 - modcause: ModerationCause 21 + modcause?: ModerationCause 22 22 } 23 23 24 24 export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) { ··· 123 123 {description} 124 124 </Text> 125 125 126 - {modcause.type === 'label' && ( 126 + {modcause?.type === 'label' && ( 127 127 <> 128 128 <Divider /> 129 129 <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
+23
src/lib/hooks/useGoBack.ts
··· 1 + import {StackActions, useNavigation} from '@react-navigation/native' 2 + 3 + import {NavigationProp} from 'lib/routes/types' 4 + import {router} from '#/routes' 5 + 6 + export function useGoBack(onGoBack?: () => unknown) { 7 + const navigation = useNavigation<NavigationProp>() 8 + return () => { 9 + onGoBack?.() 10 + if (navigation.canGoBack()) { 11 + navigation.goBack() 12 + } else { 13 + navigation.navigate('HomeTab') 14 + // Checking the state for routes ensures that web doesn't encounter errors while going back 15 + if (navigation.getState()?.routes) { 16 + navigation.dispatch(StackActions.push(...router.matchPath('/'))) 17 + } else { 18 + navigation.navigate('HomeTab') 19 + navigation.dispatch(StackActions.popToTop()) 20 + } 21 + } 22 + } 23 + }
+216
src/screens/List/ListHiddenScreen.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {AppBskyGraphDefs} from '@atproto/api' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {useQueryClient} from '@tanstack/react-query' 7 + 8 + import {logger} from '#/logger' 9 + import {RQKEY_ROOT as listQueryRoot} from '#/state/queries/list' 10 + import {useGoBack} from 'lib/hooks/useGoBack' 11 + import {sanitizeHandle} from 'lib/strings/handles' 12 + import {useListBlockMutation, useListMuteMutation} from 'state/queries/list' 13 + import { 14 + UsePreferencesQueryResponse, 15 + useRemoveFeedMutation, 16 + } from 'state/queries/preferences' 17 + import {useSession} from 'state/session' 18 + import * as Toast from 'view/com/util/Toast' 19 + import {CenteredView} from 'view/com/util/Views' 20 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 21 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 22 + import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 23 + import {Loader} from '#/components/Loader' 24 + import {useHider} from '#/components/moderation/Hider' 25 + import {Text} from '#/components/Typography' 26 + 27 + export function ListHiddenScreen({ 28 + list, 29 + preferences, 30 + }: { 31 + list: AppBskyGraphDefs.ListView 32 + preferences: UsePreferencesQueryResponse 33 + }) { 34 + const {_} = useLingui() 35 + const t = useTheme() 36 + const {currentAccount} = useSession() 37 + const {gtMobile} = useBreakpoints() 38 + const isOwner = currentAccount?.did === list.creator.did 39 + const goBack = useGoBack() 40 + const queryClient = useQueryClient() 41 + 42 + const isModList = list.purpose === AppBskyGraphDefs.MODLIST 43 + 44 + const [isProcessing, setIsProcessing] = React.useState(false) 45 + const listBlockMutation = useListBlockMutation() 46 + const listMuteMutation = useListMuteMutation() 47 + const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation() 48 + 49 + const {setIsContentVisible} = useHider() 50 + 51 + const savedFeedConfig = preferences.savedFeeds.find(f => f.value === list.uri) 52 + 53 + const onUnsubscribe = async () => { 54 + setIsProcessing(true) 55 + if (list.viewer?.muted) { 56 + try { 57 + await listMuteMutation.mutateAsync({uri: list.uri, mute: false}) 58 + } catch (e) { 59 + setIsProcessing(false) 60 + logger.error('Failed to unmute list', {message: e}) 61 + Toast.show( 62 + _( 63 + msg`There was an issue. Please check your internet connection and try again.`, 64 + ), 65 + ) 66 + return 67 + } 68 + } 69 + if (list.viewer?.blocked) { 70 + try { 71 + await listBlockMutation.mutateAsync({uri: list.uri, block: false}) 72 + } catch (e) { 73 + setIsProcessing(false) 74 + logger.error('Failed to unblock list', {message: e}) 75 + Toast.show( 76 + _( 77 + msg`There was an issue. Please check your internet connection and try again.`, 78 + ), 79 + ) 80 + return 81 + } 82 + } 83 + queryClient.invalidateQueries({ 84 + queryKey: [listQueryRoot], 85 + }) 86 + Toast.show(_(msg`Unsubscribed from list`)) 87 + setIsProcessing(false) 88 + } 89 + 90 + const onRemoveList = async () => { 91 + if (!savedFeedConfig) return 92 + try { 93 + await removeSavedFeed(savedFeedConfig) 94 + Toast.show(_(msg`Removed from saved feeds`)) 95 + } catch (e) { 96 + logger.error('Failed to remove list from saved feeds', {message: e}) 97 + Toast.show( 98 + _( 99 + msg`There was an issue. Please check your internet connection and try again.`, 100 + ), 101 + ) 102 + } finally { 103 + setIsProcessing(false) 104 + } 105 + } 106 + 107 + return ( 108 + <CenteredView 109 + style={[ 110 + a.flex_1, 111 + a.align_center, 112 + a.gap_5xl, 113 + !gtMobile && a.justify_between, 114 + t.atoms.border_contrast_low, 115 + {paddingTop: 175, paddingBottom: 110}, 116 + ]} 117 + sideBorders={true}> 118 + <View style={[a.w_full, a.align_center, a.gap_lg]}> 119 + <EyeSlash 120 + style={{color: t.atoms.text_contrast_medium.color}} 121 + height={42} 122 + width={42} 123 + /> 124 + <View style={[a.gap_sm, a.align_center]}> 125 + <Text style={[a.font_bold, a.text_3xl]}> 126 + <Trans>List has been hidden</Trans> 127 + </Text> 128 + <Text 129 + style={[ 130 + a.text_md, 131 + a.text_center, 132 + a.px_md, 133 + t.atoms.text_contrast_high, 134 + {lineHeight: 1.4}, 135 + ]}> 136 + <Trans> 137 + This list - created by{' '} 138 + <Text style={[a.text_md, !isOwner && a.font_bold]}> 139 + {isOwner 140 + ? _(msg`you`) 141 + : sanitizeHandle(list.creator.handle, '@')} 142 + </Text>{' '} 143 + - contains possible violations of Bluesky's community guidelines 144 + in its name or description. 145 + </Trans> 146 + </Text> 147 + </View> 148 + </View> 149 + <View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}> 150 + <View style={[a.gap_md]}> 151 + {savedFeedConfig ? ( 152 + <Button 153 + variant="solid" 154 + color="secondary" 155 + size="medium" 156 + label={_(msg`Remove from saved feeds`)} 157 + onPress={onRemoveList} 158 + disabled={isProcessing}> 159 + <ButtonText> 160 + <Trans>Removed from saved feeds</Trans> 161 + </ButtonText> 162 + {isProcessing ? ( 163 + <ButtonIcon icon={Loader} position="right" /> 164 + ) : null} 165 + </Button> 166 + ) : null} 167 + {isOwner ? ( 168 + <Button 169 + variant="solid" 170 + color="secondary" 171 + size="medium" 172 + label={_(msg`Show list anyway`)} 173 + onPress={() => setIsContentVisible(true)} 174 + disabled={isProcessing}> 175 + <ButtonText> 176 + <Trans>Show anyway</Trans> 177 + </ButtonText> 178 + </Button> 179 + ) : list.viewer?.muted || list.viewer?.blocked ? ( 180 + <Button 181 + variant="solid" 182 + color="secondary" 183 + size="medium" 184 + label={_(msg`Unsubscribe from list`)} 185 + onPress={() => { 186 + if (isModList) { 187 + onUnsubscribe() 188 + } else { 189 + onRemoveList() 190 + } 191 + }} 192 + disabled={isProcessing}> 193 + <ButtonText> 194 + <Trans>Unsubscribe from list</Trans> 195 + </ButtonText> 196 + {isProcessing ? ( 197 + <ButtonIcon icon={Loader} position="right" /> 198 + ) : null} 199 + </Button> 200 + ) : null} 201 + </View> 202 + <Button 203 + variant="solid" 204 + color="primary" 205 + label={_(msg`Return to previous page`)} 206 + onPress={goBack} 207 + size="medium" 208 + disabled={isProcessing}> 209 + <ButtonText> 210 + <Trans>Go Back</Trans> 211 + </ButtonText> 212 + </Button> 213 + </View> 214 + </CenteredView> 215 + ) 216 + }
+1 -1
src/state/queries/list.ts
··· 17 17 import {invalidate as invalidateMyLists} from './my-lists' 18 18 import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' 19 19 20 - const RQKEY_ROOT = 'list' 20 + export const RQKEY_ROOT = 'list' 21 21 export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] 22 22 23 23 export function useListQuery(uri?: string) {
-183
src/view/com/lists/ListCard.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import {AppBskyGraphDefs, AtUri, RichText} from '@atproto/api' 4 - import {Trans} from '@lingui/macro' 5 - 6 - import {useSession} from '#/state/session' 7 - import {usePalette} from 'lib/hooks/usePalette' 8 - import {makeProfileLink} from 'lib/routes/links' 9 - import {sanitizeDisplayName} from 'lib/strings/display-names' 10 - import {sanitizeHandle} from 'lib/strings/handles' 11 - import {s} from 'lib/styles' 12 - import {atoms as a} from '#/alf' 13 - import {RichText as RichTextCom} from '#/components/RichText' 14 - import {Link} from '../util/Link' 15 - import {Text} from '../util/text/Text' 16 - import {UserAvatar} from '../util/UserAvatar' 17 - 18 - export const ListCard = ({ 19 - testID, 20 - list, 21 - noBg, 22 - noBorder, 23 - renderButton, 24 - style, 25 - }: { 26 - testID?: string 27 - list: AppBskyGraphDefs.ListView 28 - noBg?: boolean 29 - noBorder?: boolean 30 - renderButton?: () => JSX.Element 31 - style?: StyleProp<ViewStyle> 32 - }) => { 33 - const pal = usePalette('default') 34 - const {currentAccount} = useSession() 35 - 36 - const rkey = React.useMemo(() => { 37 - try { 38 - const urip = new AtUri(list.uri) 39 - return urip.rkey 40 - } catch { 41 - return '' 42 - } 43 - }, [list]) 44 - 45 - const descriptionRichText = React.useMemo(() => { 46 - if (list.description) { 47 - return new RichText({ 48 - text: list.description, 49 - facets: list.descriptionFacets, 50 - }) 51 - } 52 - return undefined 53 - }, [list]) 54 - 55 - return ( 56 - <Link 57 - testID={testID} 58 - style={[ 59 - styles.outer, 60 - pal.border, 61 - noBorder && styles.outerNoBorder, 62 - !noBg && pal.view, 63 - style, 64 - ]} 65 - href={makeProfileLink(list.creator, 'lists', rkey)} 66 - title={list.name} 67 - asAnchor 68 - anchorNoUnderline> 69 - <View style={styles.layout}> 70 - <View style={styles.layoutAvi}> 71 - <UserAvatar type="list" size={40} avatar={list.avatar} /> 72 - </View> 73 - <View style={styles.layoutContent}> 74 - <Text 75 - type="lg" 76 - style={[s.bold, pal.text]} 77 - numberOfLines={1} 78 - lineHeight={1.2}> 79 - {sanitizeDisplayName(list.name)} 80 - </Text> 81 - <Text type="md" style={[pal.textLight]} numberOfLines={1}> 82 - {list.purpose === 'app.bsky.graph.defs#curatelist' && 83 - (list.creator.did === currentAccount?.did ? ( 84 - <Trans>User list by you</Trans> 85 - ) : ( 86 - <Trans> 87 - User list by {sanitizeHandle(list.creator.handle, '@')} 88 - </Trans> 89 - ))} 90 - {list.purpose === 'app.bsky.graph.defs#modlist' && 91 - (list.creator.did === currentAccount?.did ? ( 92 - <Trans>Moderation list by you</Trans> 93 - ) : ( 94 - <Trans> 95 - Moderation list by {sanitizeHandle(list.creator.handle, '@')} 96 - </Trans> 97 - ))} 98 - </Text> 99 - <View style={s.flexRow}> 100 - {list.viewer?.muted ? ( 101 - <View style={[s.mt5, pal.btn, styles.pill]}> 102 - <Text type="xs" style={pal.text}> 103 - <Trans>Muted</Trans> 104 - </Text> 105 - </View> 106 - ) : null} 107 - 108 - {list.viewer?.blocked ? ( 109 - <View style={[s.mt5, pal.btn, styles.pill]}> 110 - <Text type="xs" style={pal.text}> 111 - <Trans>Blocked</Trans> 112 - </Text> 113 - </View> 114 - ) : null} 115 - </View> 116 - </View> 117 - {renderButton ? ( 118 - <View style={styles.layoutButton}>{renderButton()}</View> 119 - ) : undefined} 120 - </View> 121 - {descriptionRichText ? ( 122 - <View style={styles.details}> 123 - <RichTextCom 124 - style={[a.flex_1]} 125 - numberOfLines={20} 126 - value={descriptionRichText} 127 - /> 128 - </View> 129 - ) : undefined} 130 - </Link> 131 - ) 132 - } 133 - 134 - const styles = StyleSheet.create({ 135 - outer: { 136 - borderTopWidth: StyleSheet.hairlineWidth, 137 - paddingHorizontal: 6, 138 - }, 139 - outerNoBorder: { 140 - borderTopWidth: 0, 141 - }, 142 - layout: { 143 - flexDirection: 'row', 144 - alignItems: 'center', 145 - }, 146 - layoutAvi: { 147 - width: 54, 148 - paddingLeft: 4, 149 - paddingTop: 8, 150 - paddingBottom: 10, 151 - }, 152 - avi: { 153 - width: 40, 154 - height: 40, 155 - borderRadius: 20, 156 - resizeMode: 'cover', 157 - }, 158 - layoutContent: { 159 - flex: 1, 160 - paddingRight: 10, 161 - paddingTop: 10, 162 - paddingBottom: 10, 163 - }, 164 - layoutButton: { 165 - paddingRight: 10, 166 - }, 167 - details: { 168 - paddingLeft: 54, 169 - paddingRight: 10, 170 - paddingBottom: 10, 171 - }, 172 - pill: { 173 - borderRadius: 4, 174 - paddingHorizontal: 6, 175 - paddingVertical: 2, 176 - }, 177 - btn: { 178 - paddingVertical: 7, 179 - borderRadius: 50, 180 - marginLeft: 6, 181 - paddingHorizontal: 14, 182 - }, 183 - })
+19 -21
src/view/com/lists/MyLists.tsx
··· 4 4 FlatList as RNFlatList, 5 5 RefreshControl, 6 6 StyleProp, 7 - StyleSheet, 8 7 View, 9 8 ViewStyle, 10 9 } from 'react-native' ··· 18 17 import {useAnalytics} from 'lib/analytics/analytics' 19 18 import {usePalette} from 'lib/hooks/usePalette' 20 19 import {s} from 'lib/styles' 20 + import {isWeb} from 'platform/detection' 21 + import {useModerationOpts} from 'state/preferences/moderation-opts' 21 22 import {EmptyState} from 'view/com/util/EmptyState' 23 + import {atoms as a, useTheme} from '#/alf' 24 + import * as ListCard from '#/components/ListCard' 22 25 import {ErrorMessage} from '../util/error/ErrorMessage' 23 26 import {List} from '../util/List' 24 - import {ListCard} from './ListCard' 25 27 26 28 const LOADING = {_reactKey: '__loading__'} 27 29 const EMPTY = {_reactKey: '__empty__'} ··· 41 43 testID?: string 42 44 }) { 43 45 const pal = usePalette('default') 46 + const t = useTheme() 44 47 const {track} = useAnalytics() 45 48 const {_} = useLingui() 49 + const moderationOpts = useModerationOpts() 46 50 const [isPTRing, setIsPTRing] = React.useState(false) 47 51 const {data, isFetching, isFetched, isError, error, refetch} = 48 52 useMyListsQuery(filter) ··· 53 57 if (isError && isEmpty) { 54 58 items = items.concat([ERROR_ITEM]) 55 59 } 56 - if (!isFetched && isFetching) { 60 + if ((!isFetched && isFetching) || !moderationOpts) { 57 61 items = items.concat([LOADING]) 58 62 } else if (isEmpty) { 59 63 items = items.concat([EMPTY]) ··· 61 65 items = items.concat(data) 62 66 } 63 67 return items 64 - }, [isError, isEmpty, isFetched, isFetching, data]) 68 + }, [isError, isEmpty, isFetched, isFetching, moderationOpts, data]) 65 69 66 70 // events 67 71 // = ··· 85 89 if (item === EMPTY) { 86 90 return ( 87 91 <EmptyState 88 - key={item._reactKey} 89 92 icon="list-ul" 90 93 message={_(msg`You have no lists.`)} 91 94 testID="listsEmpty" ··· 94 97 } else if (item === ERROR_ITEM) { 95 98 return ( 96 99 <ErrorMessage 97 - key={item._reactKey} 98 100 message={cleanError(error)} 99 101 onPressTryAgain={onRefresh} 100 102 /> 101 103 ) 102 104 } else if (item === LOADING) { 103 105 return ( 104 - <View key={item._reactKey} style={{padding: 20}}> 106 + <View style={{padding: 20}}> 105 107 <ActivityIndicator /> 106 108 </View> 107 109 ) ··· 109 111 return renderItem ? ( 110 112 renderItem(item, index) 111 113 ) : ( 112 - <ListCard 113 - key={item.uri} 114 - list={item} 115 - testID={`list-${item.name}`} 116 - style={styles.item} 117 - /> 114 + <View 115 + style={[ 116 + (index !== 0 || isWeb) && a.border_t, 117 + t.atoms.border_contrast_low, 118 + a.px_lg, 119 + a.py_lg, 120 + ]}> 121 + <ListCard.Default view={item} /> 122 + </View> 118 123 ) 119 124 }, 120 - [error, onRefresh, renderItem, _], 125 + [renderItem, t.atoms.border_contrast_low, _, error, onRefresh], 121 126 ) 122 127 123 128 if (inline) { ··· 166 171 ) 167 172 } 168 173 } 169 - 170 - const styles = StyleSheet.create({ 171 - item: { 172 - paddingHorizontal: 18, 173 - paddingVertical: 4, 174 - }, 175 - })
-32
src/view/com/util/post-embeds/ListEmbed.tsx
··· 1 - import React from 'react' 2 - import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 3 - import {usePalette} from 'lib/hooks/usePalette' 4 - import {ListCard} from 'view/com/lists/ListCard' 5 - import {AppBskyGraphDefs} from '@atproto/api' 6 - import {s} from 'lib/styles' 7 - 8 - export function ListEmbed({ 9 - item, 10 - style, 11 - }: { 12 - item: AppBskyGraphDefs.ListView 13 - style?: StyleProp<ViewStyle> 14 - }) { 15 - const pal = usePalette('default') 16 - 17 - return ( 18 - <View style={[pal.view, pal.border, s.border1, styles.container]}> 19 - <ListCard list={item} style={[style, styles.card]} /> 20 - </View> 21 - ) 22 - } 23 - 24 - const styles = StyleSheet.create({ 25 - container: { 26 - borderRadius: 8, 27 - }, 28 - card: { 29 - borderTopWidth: 0, 30 - borderRadius: 8, 31 - }, 32 - })
+13 -3
src/view/com/util/post-embeds/index.tsx
··· 25 25 import {useModerationOpts} from '#/state/preferences/moderation-opts' 26 26 import {usePalette} from 'lib/hooks/usePalette' 27 27 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 28 - import {atoms as a} from '#/alf' 28 + import {atoms as a, useTheme} from '#/alf' 29 + import * as ListCard from '#/components/ListCard' 29 30 import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 30 31 import {ContentHider} from '../../../../components/moderation/ContentHider' 31 32 import {AutoSizedImage} from '../images/AutoSizedImage' 32 33 import {ImageLayoutGrid} from '../images/ImageLayoutGrid' 33 34 import {ExternalLinkEmbed} from './ExternalLinkEmbed' 34 - import {ListEmbed} from './ListEmbed' 35 35 import {MaybeQuoteEmbed} from './QuoteEmbed' 36 36 37 37 type Embed = ··· 203 203 const moderation = React.useMemo(() => { 204 204 return moderationOpts ? moderateUserList(view, moderationOpts) : undefined 205 205 }, [view, moderationOpts]) 206 + const t = useTheme() 206 207 207 208 return ( 208 209 <ContentHider modui={moderation?.ui('contentList')}> 209 - <ListEmbed item={view} /> 210 + <View 211 + style={[ 212 + a.border, 213 + t.atoms.border_contrast_medium, 214 + a.p_md, 215 + a.rounded_sm, 216 + a.mt_sm, 217 + ]}> 218 + <ListCard.Default view={view} /> 219 + </View> 210 220 </ContentHider> 211 221 ) 212 222 }
+85 -63
src/view/screens/ProfileList.tsx
··· 32 32 import { 33 33 useAddSavedFeedsMutation, 34 34 usePreferencesQuery, 35 + UsePreferencesQueryResponse, 35 36 useRemoveFeedMutation, 36 37 useUpdateSavedFeedsMutation, 37 38 } from '#/state/queries/preferences' ··· 67 68 import {Text} from 'view/com/util/text/Text' 68 69 import * as Toast from 'view/com/util/Toast' 69 70 import {CenteredView} from 'view/com/util/Views' 71 + import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' 70 72 import {atoms as a, useTheme} from '#/alf' 71 73 import {useDialogControl} from '#/components/Dialog' 72 - import {ScreenHider} from '#/components/moderation/ScreenHider' 74 + import * as Hider from '#/components/moderation/Hider' 73 75 import * as Prompt from '#/components/Prompt' 74 76 import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 75 77 import {RichText} from '#/components/RichText' ··· 88 90 const {data: resolvedUri, error: resolveError} = useResolveUriQuery( 89 91 AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), 90 92 ) 93 + const {data: preferences} = usePreferencesQuery() 91 94 const {data: list, error: listError} = useListQuery(resolvedUri?.uri) 92 95 const moderationOpts = useModerationOpts() 93 96 ··· 110 113 ) 111 114 } 112 115 113 - return resolvedUri && list && moderationOpts ? ( 116 + return resolvedUri && list && moderationOpts && preferences ? ( 114 117 <ProfileListScreenLoaded 115 118 {...props} 116 119 uri={resolvedUri.uri} 117 120 list={list} 118 121 moderationOpts={moderationOpts} 122 + preferences={preferences} 119 123 /> 120 124 ) : ( 121 125 <LoadingScreen /> ··· 127 131 uri, 128 132 list, 129 133 moderationOpts, 134 + preferences, 130 135 }: Props & { 131 136 uri: string 132 137 list: AppBskyGraphDefs.ListView 133 138 moderationOpts: ModerationOpts 139 + preferences: UsePreferencesQueryResponse 134 140 }) { 135 141 const {_} = useLingui() 136 142 const queryClient = useQueryClient() 137 143 const {openComposer} = useComposerControls() 138 144 const setMinimalShellMode = useSetMinimalShellMode() 145 + const {currentAccount} = useSession() 139 146 const {rkey} = route.params 140 147 const feedSectionRef = React.useRef<SectionRef>(null) 141 148 const aboutSectionRef = React.useRef<SectionRef>(null) 142 149 const {openModal} = useModalControls() 143 - const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' 150 + const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST 144 151 const isScreenFocused = useIsFocused() 152 + const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1 153 + const isOwner = currentAccount?.did === list.creator.did 145 154 146 155 const moderation = React.useMemo(() => { 147 156 return moderateUserList(list, moderationOpts) 148 157 }, [list, moderationOpts]) 149 158 150 - useSetTitle(list.name) 159 + useSetTitle(isHidden ? _(msg`List Hidden`) : list.name) 151 160 152 161 useFocusEffect( 153 162 useCallback(() => { ··· 179 188 ) 180 189 181 190 const renderHeader = useCallback(() => { 182 - return <Header rkey={rkey} list={list} /> 183 - }, [rkey, list]) 191 + return <Header rkey={rkey} list={list} preferences={preferences} /> 192 + }, [rkey, list, preferences]) 184 193 185 194 if (isCurateList) { 186 195 return ( 187 - <ScreenHider 188 - screenDescription={'list'} 189 - modui={moderation.ui('contentView')}> 196 + <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}> 197 + <Hider.Mask> 198 + <ListHiddenScreen list={list} preferences={preferences} /> 199 + </Hider.Mask> 200 + <Hider.Content> 201 + <View style={s.hContentRegion}> 202 + <PagerWithHeader 203 + items={SECTION_TITLES_CURATE} 204 + isHeaderReady={true} 205 + renderHeader={renderHeader} 206 + onCurrentPageSelected={onCurrentPageSelected}> 207 + {({headerHeight, scrollElRef, isFocused}) => ( 208 + <FeedSection 209 + ref={feedSectionRef} 210 + feed={`list|${uri}`} 211 + scrollElRef={scrollElRef as ListRef} 212 + headerHeight={headerHeight} 213 + isFocused={isScreenFocused && isFocused} 214 + /> 215 + )} 216 + {({headerHeight, scrollElRef}) => ( 217 + <AboutSection 218 + ref={aboutSectionRef} 219 + scrollElRef={scrollElRef as ListRef} 220 + list={list} 221 + onPressAddUser={onPressAddUser} 222 + headerHeight={headerHeight} 223 + /> 224 + )} 225 + </PagerWithHeader> 226 + <FAB 227 + testID="composeFAB" 228 + onPress={() => openComposer({})} 229 + icon={ 230 + <ComposeIcon2 231 + strokeWidth={1.5} 232 + size={29} 233 + style={{color: 'white'}} 234 + /> 235 + } 236 + accessibilityRole="button" 237 + accessibilityLabel={_(msg`New post`)} 238 + accessibilityHint="" 239 + /> 240 + </View> 241 + </Hider.Content> 242 + </Hider.Outer> 243 + ) 244 + } 245 + return ( 246 + <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}> 247 + <Hider.Mask> 248 + <ListHiddenScreen list={list} preferences={preferences} /> 249 + </Hider.Mask> 250 + <Hider.Content> 190 251 <View style={s.hContentRegion}> 191 252 <PagerWithHeader 192 - items={SECTION_TITLES_CURATE} 253 + items={SECTION_TITLES_MOD} 193 254 isHeaderReady={true} 194 - renderHeader={renderHeader} 195 - onCurrentPageSelected={onCurrentPageSelected}> 196 - {({headerHeight, scrollElRef, isFocused}) => ( 197 - <FeedSection 198 - ref={feedSectionRef} 199 - feed={`list|${uri}`} 200 - scrollElRef={scrollElRef as ListRef} 201 - headerHeight={headerHeight} 202 - isFocused={isScreenFocused && isFocused} 203 - /> 204 - )} 255 + renderHeader={renderHeader}> 205 256 {({headerHeight, scrollElRef}) => ( 206 257 <AboutSection 207 - ref={aboutSectionRef} 208 - scrollElRef={scrollElRef as ListRef} 209 258 list={list} 259 + scrollElRef={scrollElRef as ListRef} 210 260 onPressAddUser={onPressAddUser} 211 261 headerHeight={headerHeight} 212 262 /> ··· 227 277 accessibilityHint="" 228 278 /> 229 279 </View> 230 - </ScreenHider> 231 - ) 232 - } 233 - return ( 234 - <ScreenHider 235 - screenDescription={_(msg`list`)} 236 - modui={moderation.ui('contentView')}> 237 - <View style={s.hContentRegion}> 238 - <PagerWithHeader 239 - items={SECTION_TITLES_MOD} 240 - isHeaderReady={true} 241 - renderHeader={renderHeader}> 242 - {({headerHeight, scrollElRef}) => ( 243 - <AboutSection 244 - list={list} 245 - scrollElRef={scrollElRef as ListRef} 246 - onPressAddUser={onPressAddUser} 247 - headerHeight={headerHeight} 248 - /> 249 - )} 250 - </PagerWithHeader> 251 - <FAB 252 - testID="composeFAB" 253 - onPress={() => openComposer({})} 254 - icon={ 255 - <ComposeIcon2 256 - strokeWidth={1.5} 257 - size={29} 258 - style={{color: 'white'}} 259 - /> 260 - } 261 - accessibilityRole="button" 262 - accessibilityLabel={_(msg`New post`)} 263 - accessibilityHint="" 264 - /> 265 - </View> 266 - </ScreenHider> 280 + </Hider.Content> 281 + </Hider.Outer> 267 282 ) 268 283 } 269 284 270 - function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { 285 + function Header({ 286 + rkey, 287 + list, 288 + preferences, 289 + }: { 290 + rkey: string 291 + list: AppBskyGraphDefs.ListView 292 + preferences: UsePreferencesQueryResponse 293 + }) { 271 294 const pal = usePalette('default') 272 295 const palInverted = usePalette('inverted') 273 296 const {_} = useLingui() ··· 283 306 const isBlocking = !!list.viewer?.blocked 284 307 const isMuting = !!list.viewer?.muted 285 308 const isOwner = list.creator.did === currentAccount?.did 286 - const {data: preferences} = usePreferencesQuery() 287 309 const {track} = useAnalytics() 288 310 const playHaptic = useHaptics() 289 311 ··· 644 666 cid: list.cid, 645 667 }} 646 668 /> 647 - {isCurateList || isPinned ? ( 669 + {isCurateList ? ( 648 670 <Button 649 671 testID={isPinned ? 'unpinBtn' : 'pinBtn'} 650 672 type={isPinned ? 'default' : 'inverted'}