Compare commits

...

7 Commits

Author SHA1 Message Date
Hailey 51e0c3cbda add learn more link 2024-08-20 09:17:30 -07:00
Hailey 6d65f5e8cb rm ts-ignore 2024-08-20 09:01:22 -07:00
Hailey a1d2f670fb tweak rendering 2024-08-20 08:57:44 -07:00
Eric Bailey f3ade055e9 Add Hider component 2024-08-20 08:56:37 -05:00
Hailey 23b010feb4 tweak styles 2024-08-19 17:53:52 -07:00
Hailey 1086aa275c tweak styles 2024-08-19 17:51:12 -07:00
Hailey 216d3cd9ae add feed hidden screen 2024-08-19 17:41:51 -07:00
5 changed files with 428 additions and 90 deletions
+4 -28
View File
@@ -2,21 +2,18 @@ import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
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 {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {router} from '#/routes'
export function Error({
title,
message,
onRetry,
onGoBack: onGoBackProp,
onGoBack,
hideBackButton,
sideBorders = true,
}: {
@@ -27,31 +24,10 @@ export function Error({
hideBackButton?: boolean
sideBorders?: boolean
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
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])
const goBack = useGoBack(onGoBack)
return (
<CenteredView
@@ -96,7 +72,7 @@ export function Error({
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label={_(msg`Return to previous page`)}
onPress={onGoBack}
onPress={goBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
<ButtonText>
+83
View File
@@ -0,0 +1,83 @@
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
noOverride: boolean
}
}
const Context = React.createContext<Context>({} as Context)
export const useScreenHider = () => React.useContext(Context)
export function Outer({
modui,
children,
}: React.PropsWithChildren<{
modui: ModerationUI
}>) {
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const [isContentVisible, setIsContentVisible] = React.useState(!blur)
const info = useModerationCauseDescription(blur)
const meta = {
isNoPwi: Boolean(
modui.blurs.find(
cause =>
cause.type === 'label' &&
cause.labelDef.identifier === '!no-unauthenticated',
),
),
noOverride: modui.noOverride,
}
const showInfoDialog = () => {
control.open()
}
const onSetContentVisible = (show: boolean) => {
if (meta.noOverride) 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 = useScreenHider()
return ctx.isContentVisible ? children : null
}
export function Mask({children}: {children: React.ReactNode}) {
const ctx = useScreenHider()
return ctx.isContentVisible ? null : children
}
+23
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())
}
}
}
}
+236
View File
@@ -0,0 +1,236 @@
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 {logger} from '#/logger'
import {useGoBack} from 'lib/hooks/useGoBack'
import {isWeb} from 'platform/detection'
import {
useListBlockMutation,
useListDeleteMutation,
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, ButtonText} from '#/components/Button'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {useScreenHider} 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 {showInfoDialog} = useScreenHider()
const goBack = useGoBack()
const isModList = list.purpose === 'app.bsky.graph.defs#modlist'
const [isProcessing, setIsProcessing] = React.useState(false)
const listBlockMutation = useListBlockMutation()
const listMuteMutation = useListMuteMutation()
const listDeleteMutation = useListDeleteMutation()
const {mutateAsync: removeSavedFeed} = useRemoveFeedMutation()
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
}
}
Toast.show(_(msg`Unsubscribed from list`))
setIsProcessing(false)
}
const onDeleteList = async () => {
setIsProcessing(true)
try {
await listDeleteMutation.mutateAsync({uri: list.uri})
Toast.show(_(msg`List deleted`))
} catch (e) {
logger.error('Failed to delete list from saved feeds', {message: e})
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
} finally {
setIsProcessing(false)
goBack()
}
}
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_xl]}>
<EyeSlash
style={{color: t.atoms.text_contrast_medium.color}}
height={42}
width={42}
/>
<View style={[a.gap_md, a.align_center]}>
<Text style={[a.font_bold, a.text_3xl]}>
<Trans>List hidden</Trans>
</Text>
<Text
style={[
a.text_md,
a.text_center,
a.px_md,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
isWeb && {maxWidth: 450},
]}>
{isOwner ? (
<Trans>
The list you are trying to view (
<Text style={[a.font_bold, a.text_md]}>{list.name}</Text>) has
been hidden.
</Trans>
) : (
<Trans>The list you are trying to view has been hidden.</Trans>
)}{' '}
<Text
style={[a.text_md, {color: t.palette.primary_500}]}
onPress={showInfoDialog}>
{isWeb ? (
<Trans>Click here to learn more</Trans>
) : (
<Trans>Tap here to learn more</Trans>
)}
</Text>
.
</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>
</Button>
) : null}
{isOwner ? (
<Button
variant="solid"
color="secondary"
size="medium"
label={_(msg`Delete List`)}
onPress={onDeleteList}
disabled={isProcessing}>
<ButtonText>
<Trans>Delete list</Trans>
</ButtonText>
</Button>
) : null}
{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>
</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>
)
}
+82 -62
View File
@@ -32,6 +32,7 @@ import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {
useAddSavedFeedsMutation,
usePreferencesQuery,
UsePreferencesQueryResponse,
useRemoveFeedMutation,
useUpdateSavedFeedsMutation,
} from '#/state/queries/preferences'
@@ -67,9 +68,10 @@ import {LoadingScreen} from 'view/com/util/LoadingScreen'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {CenteredView} from 'view/com/util/Views'
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
import {atoms as a, useTheme} from '#/alf'
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 {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
@@ -88,6 +90,7 @@ export function ProfileListScreen(props: Props) {
const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
)
const {data: preferences} = usePreferencesQuery()
const {data: list, error: listError} = useListQuery(resolvedUri?.uri)
const moderationOpts = useModerationOpts()
@@ -110,12 +113,13 @@ export function ProfileListScreen(props: Props) {
)
}
return resolvedUri && list && moderationOpts ? (
return resolvedUri && list && moderationOpts && preferences ? (
<ProfileListScreenLoaded
{...props}
uri={resolvedUri.uri}
list={list}
moderationOpts={moderationOpts}
preferences={preferences}
/>
) : (
<LoadingScreen />
@@ -127,10 +131,12 @@ function ProfileListScreenLoaded({
uri,
list,
moderationOpts,
preferences,
}: Props & {
uri: string
list: AppBskyGraphDefs.ListView
moderationOpts: ModerationOpts
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const queryClient = useQueryClient()
@@ -142,12 +148,13 @@ function ProfileListScreenLoaded({
const {openModal} = useModalControls()
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isScreenFocused = useIsFocused()
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
const moderation = React.useMemo(() => {
return moderateUserList(list, moderationOpts)
}, [list, moderationOpts])
useSetTitle(list.name)
useSetTitle(isHidden ? _(msg`List Hidden`) : list.name)
useFocusEffect(
useCallback(() => {
@@ -179,34 +186,75 @@ function ProfileListScreenLoaded({
)
const renderHeader = useCallback(() => {
return <Header rkey={rkey} list={list} />
}, [rkey, list])
return <Header rkey={rkey} list={list} preferences={preferences} />
}, [rkey, list, preferences])
if (isCurateList) {
return (
<ScreenHider
screenDescription={'list'}
modui={moderation.ui('contentView')}>
<Hider.Outer modui={moderation.ui('contentView')}>
<Hider.Mask>
<ListHiddenScreen list={list} preferences={preferences} />
</Hider.Mask>
<Hider.Content>
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES_CURATE}
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`list|${uri}`}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isFocused={isScreenFocused && isFocused}
/>
)}
{({headerHeight, scrollElRef}) => (
<AboutSection
ref={aboutSectionRef}
scrollElRef={scrollElRef as ListRef}
list={list}
onPressAddUser={onPressAddUser}
headerHeight={headerHeight}
/>
)}
</PagerWithHeader>
<FAB
testID="composeFAB"
onPress={() => openComposer({})}
icon={
<ComposeIcon2
strokeWidth={1.5}
size={29}
style={{color: 'white'}}
/>
}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
</Hider.Content>
</Hider.Outer>
)
}
return (
<Hider.Outer modui={moderation.ui('contentView')}>
<Hider.Mask>
<ListHiddenScreen list={list} preferences={preferences} />
</Hider.Mask>
<Hider.Content>
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES_CURATE}
items={SECTION_TITLES_MOD}
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`list|${uri}`}
scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight}
isFocused={isScreenFocused && isFocused}
/>
)}
renderHeader={renderHeader}>
{({headerHeight, scrollElRef}) => (
<AboutSection
ref={aboutSectionRef}
scrollElRef={scrollElRef as ListRef}
list={list}
scrollElRef={scrollElRef as ListRef}
onPressAddUser={onPressAddUser}
headerHeight={headerHeight}
/>
@@ -227,47 +275,20 @@ function ProfileListScreenLoaded({
accessibilityHint=""
/>
</View>
</ScreenHider>
)
}
return (
<ScreenHider
screenDescription={_(msg`list`)}
modui={moderation.ui('contentView')}>
<View style={s.hContentRegion}>
<PagerWithHeader
items={SECTION_TITLES_MOD}
isHeaderReady={true}
renderHeader={renderHeader}>
{({headerHeight, scrollElRef}) => (
<AboutSection
list={list}
scrollElRef={scrollElRef as ListRef}
onPressAddUser={onPressAddUser}
headerHeight={headerHeight}
/>
)}
</PagerWithHeader>
<FAB
testID="composeFAB"
onPress={() => openComposer({})}
icon={
<ComposeIcon2
strokeWidth={1.5}
size={29}
style={{color: 'white'}}
/>
}
accessibilityRole="button"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</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 palInverted = usePalette('inverted')
const {_} = useLingui()
@@ -283,7 +304,6 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted
const isOwner = list.creator.did === currentAccount?.did
const {data: preferences} = usePreferencesQuery()
const {track} = useAnalytics()
const playHaptic = useHaptics()
@@ -644,7 +664,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
cid: list.cid,
}}
/>
{isCurateList || isPinned ? (
{isCurateList ? (
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
type={isPinned ? 'default' : 'inverted'}