diff --git a/src/components/Error.tsx b/src/components/Error.tsx index 481532434a..59d219831c 100644 --- a/src/components/Error.tsx +++ b/src/components/Error.tsx @@ -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() 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 ( diff --git a/src/lib/hooks/useGoBack.ts b/src/lib/hooks/useGoBack.ts new file mode 100644 index 0000000000..59555bdac1 --- /dev/null +++ b/src/lib/hooks/useGoBack.ts @@ -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() + 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()) + } + } + } +} diff --git a/src/screens/List/ListHiddenScreen.tsx b/src/screens/List/ListHiddenScreen.tsx new file mode 100644 index 0000000000..46a114e10d --- /dev/null +++ b/src/screens/List/ListHiddenScreen.tsx @@ -0,0 +1,222 @@ +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 { + 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} from '#/components/icons/EyeSlash' +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 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 ( + + + + + + List hidden + + + {isOwner ? ( + + The list you are trying to view ( + {list.name}) has + been hidden. + + ) : ( + The list you are trying to view has been hidden. + )} + + + + + + {savedFeedConfig ? ( + + ) : null} + {isOwner ? ( + + ) : null} + {list.viewer?.muted || list.viewer?.blocked ? ( + + ) : null} + + + + + ) +} diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index bf13791ae6..4c73fed0d1 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -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,6 +68,7 @@ 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' @@ -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 ? ( ) : ( @@ -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,6 +148,7 @@ 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) @@ -179,8 +186,12 @@ function ProfileListScreenLoaded({ ) const renderHeader = useCallback(() => { - return
- }, [rkey, list]) + return
+ }, [rkey, list, preferences]) + + if (isHidden) { + return + } if (isCurateList) { return ( @@ -267,7 +278,15 @@ function ProfileListScreenLoaded({ ) } -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 +302,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 +662,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { cid: list.cid, }} /> - {isCurateList || isPinned ? ( + {isCurateList ? (