Add `list hidden` screen (#4958)

Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Hailey 2024-08-20 15:43:40 -07:00 committed by GitHub
parent e54298ec2c
commit 723896a45f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 494 additions and 339 deletions

View File

@ -2,21 +2,18 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {useGoBack} from 'lib/hooks/useGoBack'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {router} from '#/routes'
export function Error({ export function Error({
title, title,
message, message,
onRetry, onRetry,
onGoBack: onGoBackProp, onGoBack,
hideBackButton, hideBackButton,
sideBorders = true, sideBorders = true,
}: { }: {
@ -27,31 +24,10 @@ export function Error({
hideBackButton?: boolean hideBackButton?: boolean
sideBorders?: boolean sideBorders?: boolean
}) { }) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const goBack = useGoBack(onGoBack)
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (onGoBackProp) {
onGoBackProp()
return
}
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack, onGoBackProp])
return ( return (
<CenteredView <CenteredView
@ -96,7 +72,7 @@ export function Error({
variant="solid" variant="solid"
color={onRetry ? 'secondary' : 'primary'} color={onRetry ? 'secondary' : 'primary'}
label={_(msg`Return to previous page`)} label={_(msg`Return to previous page`)}
onPress={onGoBack} onPress={goBack}
size="large" size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}> style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText> <ButtonText>

View File

@ -1,13 +1,20 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs, AtUri} from '@atproto/api' import {
AppBskyActorDefs,
AppBskyGraphDefs,
AtUri,
moderateUserList,
ModerationUI,
} from '@atproto/api'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {precacheList} from 'state/queries/feed' import {precacheList} from 'state/queries/feed'
import {useTheme} from '#/alf' import {useSession} from 'state/session'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import { import {
Avatar, Avatar,
Description, Description,
@ -16,6 +23,7 @@ import {
SaveButton, SaveButton,
} from '#/components/FeedCard' } from '#/components/FeedCard'
import {Link as InternalLink, LinkProps} from '#/components/Link' import {Link as InternalLink, LinkProps} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
/* /*
@ -43,6 +51,11 @@ type Props = {
export function Default(props: Props) { export function Default(props: Props) {
const {view, showPinButton} = props const {view, showPinButton} = props
const moderationOpts = useModerationOpts()
const moderation = moderationOpts
? moderateUserList(view, moderationOpts)
: undefined
return ( return (
<Link {...props}> <Link {...props}>
<Outer> <Outer>
@ -52,6 +65,7 @@ export function Default(props: Props) {
title={view.name} title={view.name}
creator={view.creator} creator={view.creator}
purpose={view.purpose} purpose={view.purpose}
modUi={moderation?.ui('contentView')}
/> />
{showPinButton && view.purpose === CURATELIST && ( {showPinButton && view.purpose === CURATELIST && (
<SaveButton view={view} pin /> <SaveButton view={view} pin />
@ -89,18 +103,40 @@ export function TitleAndByline({
title, title,
creator, creator,
purpose = CURATELIST, purpose = CURATELIST,
modUi,
}: { }: {
title: string title: string
creator?: AppBskyActorDefs.ProfileViewBasic creator?: AppBskyActorDefs.ProfileViewBasic
purpose?: AppBskyGraphDefs.ListView['purpose'] purpose?: AppBskyGraphDefs.ListView['purpose']
modUi?: ModerationUI
}) { }) {
const t = useTheme() const t = useTheme()
const {currentAccount} = useSession()
return ( return (
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}> <Hider.Outer
modui={modUi}
isContentVisibleInitialState={
creator && currentAccount?.did === creator.did
}
allowOverride={creator && currentAccount?.did === creator.did}>
<Hider.Mask>
<Text
style={[a.text_md, a.font_bold, a.leading_snug, a.italic]}
numberOfLines={1}>
<Trans>Hidden list</Trans>
</Text>
</Hider.Mask>
<Hider.Content>
<Text
style={[a.text_md, a.font_bold, a.leading_snug]}
numberOfLines={1}>
{title} {title}
</Text> </Text>
</Hider.Content>
</Hider.Outer>
{creator && ( {creator && (
<Text <Text
style={[a.leading_snug, t.atoms.text_contrast_medium]} style={[a.leading_snug, t.atoms.text_contrast_medium]}

View File

@ -0,0 +1,89 @@
import React from 'react'
import {ModerationUI} from '@atproto/api'
import {
ModerationCauseDescription,
useModerationCauseDescription,
} from '#/lib/moderation/useModerationCauseDescription'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
type Context = {
isContentVisible: boolean
setIsContentVisible: (show: boolean) => void
info: ModerationCauseDescription
showInfoDialog: () => void
meta: {
isNoPwi: boolean
allowOverride: boolean
}
}
const Context = React.createContext<Context>({} as Context)
export const useHider = () => React.useContext(Context)
export function Outer({
modui,
isContentVisibleInitialState,
allowOverride,
children,
}: React.PropsWithChildren<{
isContentVisibleInitialState?: boolean
allowOverride?: boolean
modui: ModerationUI | undefined
}>) {
const control = useModerationDetailsDialogControl()
const blur = modui?.blurs[0]
const [isContentVisible, setIsContentVisible] = React.useState(
isContentVisibleInitialState || !blur,
)
const info = useModerationCauseDescription(blur)
const meta = {
isNoPwi: Boolean(
modui?.blurs.find(
cause =>
cause.type === 'label' &&
cause.labelDef.identifier === '!no-unauthenticated',
),
),
allowOverride: allowOverride ?? !modui?.noOverride,
}
const showInfoDialog = () => {
control.open()
}
const onSetContentVisible = (show: boolean) => {
if (meta.allowOverride) return
setIsContentVisible(show)
}
const ctx = {
isContentVisible,
setIsContentVisible: onSetContentVisible,
showInfoDialog,
info,
meta,
}
return (
<Context.Provider value={ctx}>
{children}
<ModerationDetailsDialog control={control} modcause={blur} />
</Context.Provider>
)
}
export function Content({children}: {children: React.ReactNode}) {
const ctx = useHider()
return ctx.isContentVisible ? children : null
}
export function Mask({children}: {children: React.ReactNode}) {
const ctx = useHider()
return ctx.isContentVisible ? null : children
}

View File

@ -18,7 +18,7 @@ export {useDialogControl as useModerationDetailsDialogControl} from '#/component
export interface ModerationDetailsDialogProps { export interface ModerationDetailsDialogProps {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
modcause: ModerationCause modcause?: ModerationCause
} }
export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) { export function ModerationDetailsDialog(props: ModerationDetailsDialogProps) {
@ -123,7 +123,7 @@ function ModerationDetailsDialogInner({
{description} {description}
</Text> </Text>
{modcause.type === 'label' && ( {modcause?.type === 'label' && (
<> <>
<Divider /> <Divider />
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}> <Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>

View File

@ -0,0 +1,23 @@
import {StackActions, useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {router} from '#/routes'
export function useGoBack(onGoBack?: () => unknown) {
const navigation = useNavigation<NavigationProp>()
return () => {
onGoBack?.()
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}
}

View File

@ -0,0 +1,216 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyGraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {RQKEY_ROOT as listQueryRoot} from '#/state/queries/list'
import {useGoBack} from 'lib/hooks/useGoBack'
import {sanitizeHandle} from 'lib/strings/handles'
import {useListBlockMutation, useListMuteMutation} from 'state/queries/list'
import {
UsePreferencesQueryResponse,
useRemoveFeedMutation,
} from 'state/queries/preferences'
import {useSession} from 'state/session'
import * as Toast from 'view/com/util/Toast'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Loader} from '#/components/Loader'
import {useHider} from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
export function ListHiddenScreen({
list,
preferences,
}: {
list: AppBskyGraphDefs.ListView
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const t = useTheme()
const {currentAccount} = useSession()
const {gtMobile} = useBreakpoints()
const isOwner = currentAccount?.did === list.creator.did
const goBack = useGoBack()
const queryClient = useQueryClient()
const isModList = list.purpose === AppBskyGraphDefs.MODLIST
const [isProcessing, setIsProcessing] = React.useState(false)
const listBlockMutation = useListBlockMutation()
const listMuteMutation = useListMuteMutation()
const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
const {setIsContentVisible} = useHider()
const savedFeedConfig = preferences.savedFeeds.find(f => f.value === list.uri)
const onUnsubscribe = async () => {
setIsProcessing(true)
if (list.viewer?.muted) {
try {
await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
} catch (e) {
setIsProcessing(false)
logger.error('Failed to unmute list', {message: e})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
return
}
}
if (list.viewer?.blocked) {
try {
await listBlockMutation.mutateAsync({uri: list.uri, block: false})
} catch (e) {
setIsProcessing(false)
logger.error('Failed to unblock list', {message: e})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
return
}
}
queryClient.invalidateQueries({
queryKey: [listQueryRoot],
})
Toast.show(_(msg`Unsubscribed from list`))
setIsProcessing(false)
}
const onRemoveList = async () => {
if (!savedFeedConfig) return
try {
await removeSavedFeed(savedFeedConfig)
Toast.show(_(msg`Removed from saved feeds`))
} catch (e) {
logger.error('Failed to remove list from saved feeds', {message: e})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
} finally {
setIsProcessing(false)
}
}
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
a.gap_5xl,
!gtMobile && a.justify_between,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders={true}>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<EyeSlash
style={{color: t.atoms.text_contrast_medium.color}}
height={42}
width={42}
/>
<View style={[a.gap_sm, a.align_center]}>
<Text style={[a.font_bold, a.text_3xl]}>
<Trans>List has been hidden</Trans>
</Text>
<Text
style={[
a.text_md,
a.text_center,
a.px_md,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
]}>
<Trans>
This list - created by{' '}
<Text style={[a.text_md, !isOwner && a.font_bold]}>
{isOwner
? _(msg`you`)
: sanitizeHandle(list.creator.handle, '@')}
</Text>{' '}
- contains possible violations of Bluesky's community guidelines
in its name or description.
</Trans>
</Text>
</View>
</View>
<View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
<View style={[a.gap_md]}>
{savedFeedConfig ? (
<Button
variant="solid"
color="secondary"
size="medium"
label={_(msg`Remove from saved feeds`)}
onPress={onRemoveList}
disabled={isProcessing}>
<ButtonText>
<Trans>Removed from saved feeds</Trans>
</ButtonText>
{isProcessing ? (
<ButtonIcon icon={Loader} position="right" />
) : null}
</Button>
) : null}
{isOwner ? (
<Button
variant="solid"
color="secondary"
size="medium"
label={_(msg`Show list anyway`)}
onPress={() => setIsContentVisible(true)}
disabled={isProcessing}>
<ButtonText>
<Trans>Show anyway</Trans>
</ButtonText>
</Button>
) : list.viewer?.muted || list.viewer?.blocked ? (
<Button
variant="solid"
color="secondary"
size="medium"
label={_(msg`Unsubscribe from list`)}
onPress={() => {
if (isModList) {
onUnsubscribe()
} else {
onRemoveList()
}
}}
disabled={isProcessing}>
<ButtonText>
<Trans>Unsubscribe from list</Trans>
</ButtonText>
{isProcessing ? (
<ButtonIcon icon={Loader} position="right" />
) : null}
</Button>
) : null}
</View>
<Button
variant="solid"
color="primary"
label={_(msg`Return to previous page`)}
onPress={goBack}
size="medium"
disabled={isProcessing}>
<ButtonText>
<Trans>Go Back</Trans>
</ButtonText>
</Button>
</View>
</CenteredView>
)
}

View File

@ -17,7 +17,7 @@ import {useAgent, useSession} from '../session'
import {invalidate as invalidateMyLists} from './my-lists' import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
const RQKEY_ROOT = 'list' export const RQKEY_ROOT = 'list'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListQuery(uri?: string) { export function useListQuery(uri?: string) {

View File

@ -1,183 +0,0 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {AppBskyGraphDefs, AtUri, RichText} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {useSession} from '#/state/session'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {atoms as a} from '#/alf'
import {RichText as RichTextCom} from '#/components/RichText'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
export const ListCard = ({
testID,
list,
noBg,
noBorder,
renderButton,
style,
}: {
testID?: string
list: AppBskyGraphDefs.ListView
noBg?: boolean
noBorder?: boolean
renderButton?: () => JSX.Element
style?: StyleProp<ViewStyle>
}) => {
const pal = usePalette('default')
const {currentAccount} = useSession()
const rkey = React.useMemo(() => {
try {
const urip = new AtUri(list.uri)
return urip.rkey
} catch {
return ''
}
}, [list])
const descriptionRichText = React.useMemo(() => {
if (list.description) {
return new RichText({
text: list.description,
facets: list.descriptionFacets,
})
}
return undefined
}, [list])
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
noBorder && styles.outerNoBorder,
!noBg && pal.view,
style,
]}
href={makeProfileLink(list.creator, 'lists', rkey)}
title={list.name}
asAnchor
anchorNoUnderline>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar type="list" size={40} avatar={list.avatar} />
</View>
<View style={styles.layoutContent}>
<Text
type="lg"
style={[s.bold, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(list.name)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text>
<View style={s.flexRow}>
{list.viewer?.muted ? (
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
<Trans>Muted</Trans>
</Text>
</View>
) : null}
{list.viewer?.blocked ? (
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
<Trans>Blocked</Trans>
</Text>
</View>
) : null}
</View>
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
) : undefined}
</View>
{descriptionRichText ? (
<View style={styles.details}>
<RichTextCom
style={[a.flex_1]}
numberOfLines={20}
value={descriptionRichText}
/>
</View>
) : undefined}
</Link>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
},
outerNoBorder: {
borderTopWidth: 0,
},
layout: {
flexDirection: 'row',
alignItems: 'center',
},
layoutAvi: {
width: 54,
paddingLeft: 4,
paddingTop: 8,
paddingBottom: 10,
},
avi: {
width: 40,
height: 40,
borderRadius: 20,
resizeMode: 'cover',
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
layoutButton: {
paddingRight: 10,
},
details: {
paddingLeft: 54,
paddingRight: 10,
paddingBottom: 10,
},
pill: {
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
},
btn: {
paddingVertical: 7,
borderRadius: 50,
marginLeft: 6,
paddingHorizontal: 14,
},
})

View File

@ -4,7 +4,6 @@ import {
FlatList as RNFlatList, FlatList as RNFlatList,
RefreshControl, RefreshControl,
StyleProp, StyleProp,
StyleSheet,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
@ -18,10 +17,13 @@ import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {EmptyState} from 'view/com/util/EmptyState' import {EmptyState} from 'view/com/util/EmptyState'
import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {List} from '../util/List' import {List} from '../util/List'
import {ListCard} from './ListCard'
const LOADING = {_reactKey: '__loading__'} const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'} const EMPTY = {_reactKey: '__empty__'}
@ -41,8 +43,10 @@ export function MyLists({
testID?: string testID?: string
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const t = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const {data, isFetching, isFetched, isError, error, refetch} = const {data, isFetching, isFetched, isError, error, refetch} =
useMyListsQuery(filter) useMyListsQuery(filter)
@ -53,7 +57,7 @@ export function MyLists({
if (isError && isEmpty) { if (isError && isEmpty) {
items = items.concat([ERROR_ITEM]) items = items.concat([ERROR_ITEM])
} }
if (!isFetched && isFetching) { if ((!isFetched && isFetching) || !moderationOpts) {
items = items.concat([LOADING]) items = items.concat([LOADING])
} else if (isEmpty) { } else if (isEmpty) {
items = items.concat([EMPTY]) items = items.concat([EMPTY])
@ -61,7 +65,7 @@ export function MyLists({
items = items.concat(data) items = items.concat(data)
} }
return items return items
}, [isError, isEmpty, isFetched, isFetching, data]) }, [isError, isEmpty, isFetched, isFetching, moderationOpts, data])
// events // events
// = // =
@ -85,7 +89,6 @@ export function MyLists({
if (item === EMPTY) { if (item === EMPTY) {
return ( return (
<EmptyState <EmptyState
key={item._reactKey}
icon="list-ul" icon="list-ul"
message={_(msg`You have no lists.`)} message={_(msg`You have no lists.`)}
testID="listsEmpty" testID="listsEmpty"
@ -94,14 +97,13 @@ export function MyLists({
} else if (item === ERROR_ITEM) { } else if (item === ERROR_ITEM) {
return ( return (
<ErrorMessage <ErrorMessage
key={item._reactKey}
message={cleanError(error)} message={cleanError(error)}
onPressTryAgain={onRefresh} onPressTryAgain={onRefresh}
/> />
) )
} else if (item === LOADING) { } else if (item === LOADING) {
return ( return (
<View key={item._reactKey} style={{padding: 20}}> <View style={{padding: 20}}>
<ActivityIndicator /> <ActivityIndicator />
</View> </View>
) )
@ -109,15 +111,18 @@ export function MyLists({
return renderItem ? ( return renderItem ? (
renderItem(item, index) renderItem(item, index)
) : ( ) : (
<ListCard <View
key={item.uri} style={[
list={item} (index !== 0 || isWeb) && a.border_t,
testID={`list-${item.name}`} t.atoms.border_contrast_low,
style={styles.item} a.px_lg,
/> a.py_lg,
]}>
<ListCard.Default view={item} />
</View>
) )
}, },
[error, onRefresh, renderItem, _], [renderItem, t.atoms.border_contrast_low, _, error, onRefresh],
) )
if (inline) { if (inline) {
@ -166,10 +171,3 @@ export function MyLists({
) )
} }
} }
const styles = StyleSheet.create({
item: {
paddingHorizontal: 18,
paddingVertical: 4,
},
})

View File

@ -1,32 +0,0 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {ListCard} from 'view/com/lists/ListCard'
import {AppBskyGraphDefs} from '@atproto/api'
import {s} from 'lib/styles'
export function ListEmbed({
item,
style,
}: {
item: AppBskyGraphDefs.ListView
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
return (
<View style={[pal.view, pal.border, s.border1, styles.container]}>
<ListCard list={item} style={[style, styles.card]} />
</View>
)
}
const styles = StyleSheet.create({
container: {
borderRadius: 8,
},
card: {
borderTopWidth: 0,
borderRadius: 8,
},
})

View File

@ -25,13 +25,13 @@ import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {ContentHider} from '../../../../components/moderation/ContentHider' import {ContentHider} from '../../../../components/moderation/ContentHider'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
import {ImageLayoutGrid} from '../images/ImageLayoutGrid' import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {ListEmbed} from './ListEmbed'
import {MaybeQuoteEmbed} from './QuoteEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed'
type Embed = type Embed =
@ -203,10 +203,20 @@ function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) {
const moderation = React.useMemo(() => { const moderation = React.useMemo(() => {
return moderationOpts ? moderateUserList(view, moderationOpts) : undefined return moderationOpts ? moderateUserList(view, moderationOpts) : undefined
}, [view, moderationOpts]) }, [view, moderationOpts])
const t = useTheme()
return ( return (
<ContentHider modui={moderation?.ui('contentList')}> <ContentHider modui={moderation?.ui('contentList')}>
<ListEmbed item={view} /> <View
style={[
a.border,
t.atoms.border_contrast_medium,
a.p_md,
a.rounded_sm,
a.mt_sm,
]}>
<ListCard.Default view={view} />
</View>
</ContentHider> </ContentHider>
) )
} }

View File

@ -32,6 +32,7 @@ import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import { import {
useAddSavedFeedsMutation, useAddSavedFeedsMutation,
usePreferencesQuery, usePreferencesQuery,
UsePreferencesQueryResponse,
useRemoveFeedMutation, useRemoveFeedMutation,
useUpdateSavedFeedsMutation, useUpdateSavedFeedsMutation,
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
@ -67,9 +68,10 @@ import {LoadingScreen} from 'view/com/util/LoadingScreen'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {ScreenHider} from '#/components/moderation/ScreenHider' import * as Hider from '#/components/moderation/Hider'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
@ -88,6 +90,7 @@ export function ProfileListScreen(props: Props) {
const {data: resolvedUri, error: resolveError} = useResolveUriQuery( const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
) )
const {data: preferences} = usePreferencesQuery()
const {data: list, error: listError} = useListQuery(resolvedUri?.uri) const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@ -110,12 +113,13 @@ export function ProfileListScreen(props: Props) {
) )
} }
return resolvedUri && list && moderationOpts ? ( return resolvedUri && list && moderationOpts && preferences ? (
<ProfileListScreenLoaded <ProfileListScreenLoaded
{...props} {...props}
uri={resolvedUri.uri} uri={resolvedUri.uri}
list={list} list={list}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
preferences={preferences}
/> />
) : ( ) : (
<LoadingScreen /> <LoadingScreen />
@ -127,27 +131,32 @@ function ProfileListScreenLoaded({
uri, uri,
list, list,
moderationOpts, moderationOpts,
preferences,
}: Props & { }: Props & {
uri: string uri: string
list: AppBskyGraphDefs.ListView list: AppBskyGraphDefs.ListView
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
preferences: UsePreferencesQueryResponse
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {currentAccount} = useSession()
const {rkey} = route.params const {rkey} = route.params
const feedSectionRef = React.useRef<SectionRef>(null) const feedSectionRef = React.useRef<SectionRef>(null)
const aboutSectionRef = React.useRef<SectionRef>(null) const aboutSectionRef = React.useRef<SectionRef>(null)
const {openModal} = useModalControls() const {openModal} = useModalControls()
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist' const isCurateList = list.purpose === AppBskyGraphDefs.CURATELIST
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
const isOwner = currentAccount?.did === list.creator.did
const moderation = React.useMemo(() => { const moderation = React.useMemo(() => {
return moderateUserList(list, moderationOpts) return moderateUserList(list, moderationOpts)
}, [list, moderationOpts]) }, [list, moderationOpts])
useSetTitle(list.name) useSetTitle(isHidden ? _(msg`List Hidden`) : list.name)
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@ -179,14 +188,16 @@ function ProfileListScreenLoaded({
) )
const renderHeader = useCallback(() => { const renderHeader = useCallback(() => {
return <Header rkey={rkey} list={list} /> return <Header rkey={rkey} list={list} preferences={preferences} />
}, [rkey, list]) }, [rkey, list, preferences])
if (isCurateList) { if (isCurateList) {
return ( return (
<ScreenHider <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
screenDescription={'list'} <Hider.Mask>
modui={moderation.ui('contentView')}> <ListHiddenScreen list={list} preferences={preferences} />
</Hider.Mask>
<Hider.Content>
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<PagerWithHeader <PagerWithHeader
items={SECTION_TITLES_CURATE} items={SECTION_TITLES_CURATE}
@ -227,13 +238,16 @@ function ProfileListScreenLoaded({
accessibilityHint="" accessibilityHint=""
/> />
</View> </View>
</ScreenHider> </Hider.Content>
</Hider.Outer>
) )
} }
return ( return (
<ScreenHider <Hider.Outer modui={moderation.ui('contentView')} allowOverride={isOwner}>
screenDescription={_(msg`list`)} <Hider.Mask>
modui={moderation.ui('contentView')}> <ListHiddenScreen list={list} preferences={preferences} />
</Hider.Mask>
<Hider.Content>
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<PagerWithHeader <PagerWithHeader
items={SECTION_TITLES_MOD} items={SECTION_TITLES_MOD}
@ -263,11 +277,20 @@ function ProfileListScreenLoaded({
accessibilityHint="" accessibilityHint=""
/> />
</View> </View>
</ScreenHider> </Hider.Content>
</Hider.Outer>
) )
} }
function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { function Header({
rkey,
list,
preferences,
}: {
rkey: string
list: AppBskyGraphDefs.ListView
preferences: UsePreferencesQueryResponse
}) {
const pal = usePalette('default') const pal = usePalette('default')
const palInverted = usePalette('inverted') const palInverted = usePalette('inverted')
const {_} = useLingui() const {_} = useLingui()
@ -283,7 +306,6 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const isBlocking = !!list.viewer?.blocked const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted const isMuting = !!list.viewer?.muted
const isOwner = list.creator.did === currentAccount?.did const isOwner = list.creator.did === currentAccount?.did
const {data: preferences} = usePreferencesQuery()
const {track} = useAnalytics() const {track} = useAnalytics()
const playHaptic = useHaptics() const playHaptic = useHaptics()
@ -644,7 +666,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
cid: list.cid, cid: list.cid,
}} }}
/> />
{isCurateList || isPinned ? ( {isCurateList ? (
<Button <Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'} testID={isPinned ? 'unpinBtn' : 'pinBtn'}
type={isPinned ? 'default' : 'inverted'} type={isPinned ? 'default' : 'inverted'}