diff options
Diffstat (limited to 'src')
25 files changed, 1022 insertions, 751 deletions
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 5db137e58..b7f9206e8 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -2,6 +2,7 @@ import { UsePreferencesQueryResponse, ThreadViewPreferences, } from '#/state/queries/preferences/types' +import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] = { @@ -25,3 +26,26 @@ export const DEFAULT_PROD_FEEDS = { pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')], saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')], } + +export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { + birthDate: new Date('2022-11-17'), // TODO(pwi) + adultContentEnabled: false, + feeds: { + saved: [], + pinned: [], + unpinned: [], + }, + // labels are undefined until set by user + contentLabels: { + nsfw: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nsfw, + nudity: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nudity, + suggestive: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.suggestive, + gore: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.gore, + hate: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.hate, + spam: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.spam, + impersonation: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.impersonation, + }, + feedViewPrefs: DEFAULT_HOME_FEED_PREFS, + threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS, + userAge: 13, // TODO(pwi) +} diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index e7fa3b15b..afdec267d 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -15,6 +15,7 @@ import {temp__migrateLabelPref} from '#/state/queries/preferences/util' import { DEFAULT_HOME_FEED_PREFS, DEFAULT_THREAD_VIEW_PREFS, + DEFAULT_LOGGED_OUT_PREFERENCES, } from '#/state/queries/preferences/const' import {getModerationOpts} from '#/state/queries/preferences/moderation' import {STALE} from '#/state/queries' @@ -23,63 +24,67 @@ export * from '#/state/queries/preferences/types' export * from '#/state/queries/preferences/moderation' export * from '#/state/queries/preferences/const' -export const usePreferencesQueryKey = ['getPreferences'] +export const preferencesQueryKey = ['getPreferences'] export function usePreferencesQuery() { - const {hasSession} = useSession() return useQuery({ - enabled: hasSession, staleTime: STALE.MINUTES.ONE, - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, queryFn: async () => { - const res = await getAgent().getPreferences() - const preferences: UsePreferencesQueryResponse = { - ...res, - feeds: { - saved: res.feeds?.saved || [], - pinned: res.feeds?.pinned || [], - unpinned: - res.feeds.saved?.filter(f => { - return !res.feeds.pinned?.includes(f) - }) || [], - }, - // labels are undefined until set by user - contentLabels: { - nsfw: temp__migrateLabelPref( - res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw, - ), - nudity: temp__migrateLabelPref( - res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity, - ), - suggestive: temp__migrateLabelPref( - res.contentLabels?.suggestive || - DEFAULT_LABEL_PREFERENCES.suggestive, - ), - gore: temp__migrateLabelPref( - res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore, - ), - hate: temp__migrateLabelPref( - res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate, - ), - spam: temp__migrateLabelPref( - res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam, - ), - impersonation: temp__migrateLabelPref( - res.contentLabels?.impersonation || - DEFAULT_LABEL_PREFERENCES.impersonation, - ), - }, - feedViewPrefs: { - ...DEFAULT_HOME_FEED_PREFS, - ...(res.feedViewPrefs.home || {}), - }, - threadViewPrefs: { - ...DEFAULT_THREAD_VIEW_PREFS, - ...(res.threadViewPrefs ?? {}), - }, - userAge: res.birthDate ? getAge(res.birthDate) : undefined, + const agent = getAgent() + + if (agent.session?.did === undefined) { + return DEFAULT_LOGGED_OUT_PREFERENCES + } else { + const res = await agent.getPreferences() + const preferences: UsePreferencesQueryResponse = { + ...res, + feeds: { + saved: res.feeds?.saved || [], + pinned: res.feeds?.pinned || [], + unpinned: + res.feeds.saved?.filter(f => { + return !res.feeds.pinned?.includes(f) + }) || [], + }, + // labels are undefined until set by user + contentLabels: { + nsfw: temp__migrateLabelPref( + res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw, + ), + nudity: temp__migrateLabelPref( + res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity, + ), + suggestive: temp__migrateLabelPref( + res.contentLabels?.suggestive || + DEFAULT_LABEL_PREFERENCES.suggestive, + ), + gore: temp__migrateLabelPref( + res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore, + ), + hate: temp__migrateLabelPref( + res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate, + ), + spam: temp__migrateLabelPref( + res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam, + ), + impersonation: temp__migrateLabelPref( + res.contentLabels?.impersonation || + DEFAULT_LABEL_PREFERENCES.impersonation, + ), + }, + feedViewPrefs: { + ...DEFAULT_HOME_FEED_PREFS, + ...(res.feedViewPrefs.home || {}), + }, + threadViewPrefs: { + ...DEFAULT_THREAD_VIEW_PREFS, + ...(res.threadViewPrefs ?? {}), + }, + userAge: res.birthDate ? getAge(res.birthDate) : undefined, + } + return preferences } - return preferences }, }) } @@ -107,7 +112,7 @@ export function useClearPreferencesMutation() { await getAgent().app.bsky.actor.putPreferences({preferences: []}) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -125,7 +130,7 @@ export function usePreferencesSetContentLabelMutation() { await getAgent().setContentLabelPref(labelGroup, visibility) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -139,7 +144,7 @@ export function usePreferencesSetAdultContentMutation() { await getAgent().setAdultContentEnabled(enabled) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -153,7 +158,7 @@ export function usePreferencesSetBirthDateMutation() { await getAgent().setPersonalDetails({birthDate}) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -167,7 +172,7 @@ export function useSetFeedViewPreferencesMutation() { await getAgent().setFeedViewPrefs('home', prefs) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -181,7 +186,7 @@ export function useSetThreadViewPreferencesMutation() { await getAgent().setThreadViewPrefs(prefs) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -199,7 +204,7 @@ export function useSetSaveFeedsMutation() { await getAgent().setSavedFeeds(saved, pinned) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -214,7 +219,7 @@ export function useSaveFeedMutation() { track('CustomFeed:Save') // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -229,7 +234,7 @@ export function useRemoveFeedMutation() { track('CustomFeed:Unsave') // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -244,7 +249,7 @@ export function usePinFeedMutation() { track('CustomFeed:Pin', {uri}) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) @@ -259,7 +264,7 @@ export function useUnpinFeedMutation() { track('CustomFeed:Unpin', {uri}) // triggers a refetch await queryClient.invalidateQueries({ - queryKey: usePreferencesQueryKey, + queryKey: preferencesQueryKey, }) }, }) diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts index a26380a36..cdae52937 100644 --- a/src/state/queries/preferences/moderation.ts +++ b/src/state/queries/preferences/moderation.ts @@ -34,6 +34,24 @@ export const DEFAULT_LABEL_PREFERENCES: Record< impersonation: 'hide', } +/** + * More strict than our default settings for logged in users. + * + * TODO(pwi) + */ +export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: Record< + ConfigurableLabelGroup, + LabelPreference +> = { + nsfw: 'hide', + nudity: 'hide', + suggestive: 'hide', + gore: 'hide', + hate: 'hide', + spam: 'hide', + impersonation: 'hide', +} + export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = { id: 'illegal', title: 'Illegal Content', diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index ff7420673..5fca8d558 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -43,7 +43,10 @@ export type UsePreferencesQueryResponse = Omit< } } -export type ThreadViewPreferences = Omit<BskyThreadViewPreference, 'sort'> & { +export type ThreadViewPreferences = Pick< + BskyThreadViewPreference, + 'prioritizeFollowedUsers' +> & { sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string lab_treeViewEnabled?: boolean } diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 97bd6e976..d7541295b 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -8,6 +8,7 @@ import * as persisted from '#/state/persisted' import {PUBLIC_BSKY_AGENT} from '#/state/queries' import {IS_PROD} from '#/lib/constants' import {emitSessionLoaded, emitSessionDropped} from '../events' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT @@ -515,3 +516,19 @@ export function useSession() { export function useSessionApi() { return React.useContext(ApiContext) } + +export function useRequireAuth() { + const {hasSession} = useSession() + const {setShowLoggedOut} = useLoggedOutViewControls() + + return React.useCallback( + (fn: () => void) => { + if (hasSession) { + fn() + } else { + setShowLoggedOut(true) + } + }, + [hasSession, setShowLoggedOut], + ) +} diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index 53f05055c..897a66020 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -7,6 +7,7 @@ import {Provider as ColorModeProvider} from './color-mode' import {Provider as OnboardingProvider} from './onboarding' import {Provider as ComposerProvider} from './composer' import {Provider as TickEveryMinuteProvider} from './tick-every-minute' +import {Provider as LoggedOutViewProvider} from './logged-out' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export { @@ -22,19 +23,23 @@ export {useTickEveryMinute} from './tick-every-minute' export function Provider({children}: React.PropsWithChildren<{}>) { return ( <ShellLayoutProvder> - <DrawerOpenProvider> - <DrawerSwipableProvider> - <MinimalModeProvider> - <ColorModeProvider> - <OnboardingProvider> - <ComposerProvider> - <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> - </ComposerProvider> - </OnboardingProvider> - </ColorModeProvider> - </MinimalModeProvider> - </DrawerSwipableProvider> - </DrawerOpenProvider> + <LoggedOutViewProvider> + <DrawerOpenProvider> + <DrawerSwipableProvider> + <MinimalModeProvider> + <ColorModeProvider> + <OnboardingProvider> + <ComposerProvider> + <TickEveryMinuteProvider> + {children} + </TickEveryMinuteProvider> + </ComposerProvider> + </OnboardingProvider> + </ColorModeProvider> + </MinimalModeProvider> + </DrawerSwipableProvider> + </DrawerOpenProvider> + </LoggedOutViewProvider> </ShellLayoutProvder> ) } diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx new file mode 100644 index 000000000..19eaee76b --- /dev/null +++ b/src/state/shell/logged-out.tsx @@ -0,0 +1,37 @@ +import React from 'react' + +type StateContext = { + showLoggedOut: boolean +} + +const StateContext = React.createContext<StateContext>({ + showLoggedOut: false, +}) +const ControlsContext = React.createContext<{ + setShowLoggedOut: (show: boolean) => void +}>({ + setShowLoggedOut: () => {}, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [showLoggedOut, setShowLoggedOut] = React.useState(false) + + const state = React.useMemo(() => ({showLoggedOut}), [showLoggedOut]) + const controls = React.useMemo(() => ({setShowLoggedOut}), [setShowLoggedOut]) + + return ( + <StateContext.Provider value={state}> + <ControlsContext.Provider value={controls}> + {children} + </ControlsContext.Provider> + </StateContext.Provider> + ) +} + +export function useLoggedOutView() { + return React.useContext(StateContext) +} + +export function useLoggedOutViewControls() { + return React.useContext(ControlsContext) +} diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index 3505e86af..daafa60a3 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -15,7 +15,7 @@ enum ScreenState { S_CreateAccount, } -export function LoggedOut() { +export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { const pal = usePalette('default') const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() @@ -31,6 +31,7 @@ export function LoggedOut() { if (screenState === ScreenState.S_LoginOrCreateAccount) { return ( <SplashScreen + onDismiss={onDismiss} onPressSignin={() => setScreenState(ScreenState.S_Login)} onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)} /> diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index 05e72a2e6..2c968aef4 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -1,5 +1,12 @@ import React from 'react' -import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' +import { + SafeAreaView, + StyleSheet, + TouchableOpacity, + Pressable, + View, +} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from 'view/com/util/text/Text' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {s, colors} from 'lib/styles' @@ -9,9 +16,11 @@ import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' export const SplashScreen = ({ + onDismiss, onPressSignin, onPressCreateAccount, }: { + onDismiss?: () => void onPressSignin: () => void onPressCreateAccount: () => void }) => { @@ -20,6 +29,27 @@ export const SplashScreen = ({ return ( <CenteredView style={[styles.container, pal.view]}> + {onDismiss && ( + <Pressable + accessibilityRole="button" + style={{ + position: 'absolute', + top: 20, + right: 20, + padding: 20, + zIndex: 100, + }} + onPress={onDismiss}> + <FontAwesomeIcon + icon="x" + size={24} + style={{ + color: String(pal.text.color), + }} + /> + </Pressable> + )} + <SafeAreaView testID="noSessionView" style={styles.container}> <ErrorBoundary> <View style={styles.hero}> diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index f10dc4f98..08cf701da 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -1,5 +1,6 @@ import React from 'react' -import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from 'view/com/util/text/Text' import {TextLink} from '../util/Link' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' @@ -11,9 +12,11 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {Trans} from '@lingui/macro' export const SplashScreen = ({ + onDismiss, onPressSignin, onPressCreateAccount, }: { + onDismiss?: () => void onPressSignin: () => void onPressCreateAccount: () => void }) => { @@ -23,47 +26,70 @@ export const SplashScreen = ({ const isMobileWeb = isWeb && isTabletOrMobile return ( - <CenteredView style={[styles.container, pal.view]}> - <View - testID="noSessionView" - style={[ - styles.containerInner, - isMobileWeb && styles.containerInnerMobile, - pal.border, - ]}> - <ErrorBoundary> - <Text style={isMobileWeb ? styles.titleMobile : styles.title}> - Bluesky - </Text> - <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}> - See what's next - </Text> - <View testID="signinOrCreateAccount" style={styles.btns}> - <TouchableOpacity - testID="createAccountButton" - style={[styles.btn, {backgroundColor: colors.blue3}]} - onPress={onPressCreateAccount} - // TODO: web accessibility - accessibilityRole="button"> - <Text style={[s.white, styles.btnLabel]}> - Create a new account - </Text> - </TouchableOpacity> - <TouchableOpacity - testID="signInButton" - style={[styles.btn, pal.btn]} - onPress={onPressSignin} - // TODO: web accessibility - accessibilityRole="button"> - <Text style={[pal.text, styles.btnLabel]}> - <Trans>Sign In</Trans> - </Text> - </TouchableOpacity> - </View> - </ErrorBoundary> - </View> - <Footer styles={styles} /> - </CenteredView> + <> + {onDismiss && ( + <Pressable + accessibilityRole="button" + style={{ + position: 'absolute', + top: 20, + right: 20, + padding: 20, + zIndex: 100, + }} + onPress={onDismiss}> + <FontAwesomeIcon + icon="x" + size={24} + style={{ + color: String(pal.text.color), + }} + /> + </Pressable> + )} + + <CenteredView style={[styles.container, pal.view]}> + <View + testID="noSessionView" + style={[ + styles.containerInner, + isMobileWeb && styles.containerInnerMobile, + pal.border, + ]}> + <ErrorBoundary> + <Text style={isMobileWeb ? styles.titleMobile : styles.title}> + Bluesky + </Text> + <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}> + See what's next + </Text> + <View testID="signinOrCreateAccount" style={styles.btns}> + <TouchableOpacity + testID="createAccountButton" + style={[styles.btn, {backgroundColor: colors.blue3}]} + onPress={onPressCreateAccount} + // TODO: web accessibility + accessibilityRole="button"> + <Text style={[s.white, styles.btnLabel]}> + Create a new account + </Text> + </TouchableOpacity> + <TouchableOpacity + testID="signInButton" + style={[styles.btn, pal.btn]} + onPress={onPressSignin} + // TODO: web accessibility + accessibilityRole="button"> + <Text style={[pal.text, styles.btnLabel]}> + <Trans>Sign In</Trans> + </Text> + </TouchableOpacity> + </View> + </ErrorBoundary> + </View> + <Footer styles={styles} /> + </CenteredView> + </> ) } diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx index 5fd89a8bd..7a9138545 100644 --- a/src/view/com/auth/withAuthRequired.tsx +++ b/src/view/com/auth/withAuthRequired.tsx @@ -13,18 +13,33 @@ import {usePalette} from 'lib/hooks/usePalette' import {STATUS_PAGE_URL} from 'lib/constants' import {useOnboardingState} from '#/state/shell' import {useSession} from '#/state/session' +import { + useLoggedOutView, + useLoggedOutViewControls, +} from '#/state/shell/logged-out' +import {IS_PROD} from '#/env' export const withAuthRequired = <P extends object>( Component: React.ComponentType<P>, + options: { + isPublic?: boolean // TODO(pwi) need to enable in TF somehow + } = {}, ): React.FC<P> => function AuthRequired(props: P) { const {isInitialLoad, hasSession} = useSession() const onboardingState = useOnboardingState() + const {showLoggedOut} = useLoggedOutView() + const {setShowLoggedOut} = useLoggedOutViewControls() + if (isInitialLoad) { return <Loading /> } if (!hasSession) { - return <LoggedOut /> + if (showLoggedOut) { + return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> + } else if (!options?.isPublic || IS_PROD) { + return <LoggedOut /> + } } if (onboardingState.isActive) { return <Onboarding /> diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 120aecf45..e548c45f7 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -25,6 +25,7 @@ import { } from '#/state/queries/post' import {useComposerControls} from '#/state/shell/composer' import {Shadow} from '#/state/cache/types' +import {useRequireAuth} from '#/state/session' export function PostCtrls({ big, @@ -46,6 +47,7 @@ export function PostCtrls({ const postUnlikeMutation = usePostUnlikeMutation() const postRepostMutation = usePostRepostMutation() const postUnrepostMutation = usePostUnrepostMutation() + const requireAuth = useRequireAuth() const defaultCtrlColor = React.useMemo( () => ({ @@ -107,7 +109,9 @@ export function PostCtrls({ <TouchableOpacity testID="replyBtn" style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]} - onPress={onPressReply} + onPress={() => { + requireAuth(() => onPressReply()) + }} accessibilityRole="button" accessibilityLabel={`Reply (${post.replyCount} ${ post.replyCount === 1 ? 'reply' : 'replies' @@ -135,7 +139,9 @@ export function PostCtrls({ <TouchableOpacity testID="likeBtn" style={[styles.ctrl, !big && styles.ctrlPad]} - onPress={onPressToggleLike} + onPress={() => { + requireAuth(() => onPressToggleLike()) + }} accessibilityRole="button" accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${ post.likeCount diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 0a7637252..1d34a88ab 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -7,6 +7,7 @@ import {Text} from '../text/Text' import {pluralize} from 'lib/strings/helpers' import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {useModalControls} from '#/state/modals' +import {useRequireAuth} from '#/state/session' interface Props { isReposted: boolean @@ -25,6 +26,7 @@ export const RepostButton = ({ }: Props) => { const theme = useTheme() const {openModal} = useModalControls() + const requireAuth = useRequireAuth() const defaultControlColor = React.useMemo( () => ({ @@ -45,7 +47,9 @@ export const RepostButton = ({ return ( <TouchableOpacity testID="repostBtn" - onPress={onPressToggleRepostWrapper} + onPress={() => { + requireAuth(() => onPressToggleRepostWrapper()) + }} style={[styles.control, !big && styles.controlPad]} accessibilityRole="button" accessibilityLabel={`${ diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx index 6c5f816aa..329382132 100644 --- a/src/view/com/util/post-ctrls/RepostButton.web.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {StyleProp, StyleSheet, View, ViewStyle, Pressable} from 'react-native' import {RepostIcon} from 'lib/icons' import {colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' @@ -12,6 +12,8 @@ import { import {EventStopper} from '../EventStopper' import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' +import {useRequireAuth} from '#/state/session' +import {useSession} from '#/state/session' interface Props { isReposted: boolean @@ -31,6 +33,8 @@ export const RepostButton = ({ }: Props) => { const theme = useTheme() const {_} = useLingui() + const {hasSession} = useSession() + const requireAuth = useRequireAuth() const defaultControlColor = React.useMemo( () => ({ @@ -62,32 +66,46 @@ export const RepostButton = ({ }, ] - return ( + const inner = ( + <View + style={[ + styles.control, + !big && styles.controlPad, + (isReposted + ? styles.reposted + : defaultControlColor) as StyleProp<ViewStyle>, + ]}> + <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} /> + {typeof repostCount !== 'undefined' ? ( + <Text + testID="repostCount" + type={isReposted ? 'md-bold' : 'md'} + style={styles.repostCount}> + {repostCount ?? 0} + </Text> + ) : undefined} + </View> + ) + + return hasSession ? ( <EventStopper> <NativeDropdown items={dropdownItems} accessibilityLabel={_(msg`Repost or quote post`)} accessibilityHint=""> - <View - style={[ - styles.control, - !big && styles.controlPad, - (isReposted - ? styles.reposted - : defaultControlColor) as StyleProp<ViewStyle>, - ]}> - <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} /> - {typeof repostCount !== 'undefined' ? ( - <Text - testID="repostCount" - type={isReposted ? 'md-bold' : 'md'} - style={styles.repostCount}> - {repostCount ?? 0} - </Text> - ) : undefined} - </View> + {inner} </NativeDropdown> </EventStopper> + ) : ( + <Pressable + accessibilityRole="button" + onPress={() => { + requireAuth(() => {}) + }} + accessibilityLabel={_(msg`Repost or quote post`)} + accessibilityHint=""> + {inner} + </Pressable> ) } diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index 5d62125ce..301c87d14 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -87,426 +87,429 @@ type FlatlistSlice = key: string } -export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( - _props: Props, -) { - const pal = usePalette('default') - const {openComposer} = useComposerControls() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() - const [query, setQuery] = React.useState('') - const [isPTR, setIsPTR] = React.useState(false) - const { - data: preferences, - isLoading: isPreferencesLoading, - error: preferencesError, - } = usePreferencesQuery() - const { - data: popularFeeds, - isFetching: isPopularFeedsFetching, - error: popularFeedsError, - refetch: refetchPopularFeeds, - fetchNextPage: fetchNextPopularFeedsPage, - isFetchingNextPage: isPopularFeedsFetchingNextPage, - hasNextPage: hasNextPopularFeedsPage, - } = useGetPopularFeedsQuery() - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const { - data: searchResults, - mutate: search, - reset: resetSearch, - isPending: isSearchPending, - error: searchError, - } = useSearchPopularFeedsMutation() +export const FeedsScreen = withAuthRequired( + function FeedsScreenImpl(_props: Props) { + const pal = usePalette('default') + const {openComposer} = useComposerControls() + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const [query, setQuery] = React.useState('') + const [isPTR, setIsPTR] = React.useState(false) + const { + data: preferences, + isLoading: isPreferencesLoading, + error: preferencesError, + } = usePreferencesQuery() + const { + data: popularFeeds, + isFetching: isPopularFeedsFetching, + error: popularFeedsError, + refetch: refetchPopularFeeds, + fetchNextPage: fetchNextPopularFeedsPage, + isFetchingNextPage: isPopularFeedsFetchingNextPage, + hasNextPage: hasNextPopularFeedsPage, + } = useGetPopularFeedsQuery() + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const { + data: searchResults, + mutate: search, + reset: resetSearch, + isPending: isSearchPending, + error: searchError, + } = useSearchPopularFeedsMutation() - /** - * A search query is present. We may not have search results yet. - */ - const isUserSearching = query.length > 1 - const debouncedSearch = React.useMemo( - () => debounce(q => search(q), 500), // debounce for 500ms - [search], - ) - const onPressCompose = React.useCallback(() => { - openComposer({}) - }, [openComposer]) - const onChangeQuery = React.useCallback( - (text: string) => { - setQuery(text) - if (text.length > 1) { - debouncedSearch(text) - } else { - refetchPopularFeeds() - resetSearch() - } - }, - [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch], - ) - const onPressCancelSearch = React.useCallback(() => { - setQuery('') - refetchPopularFeeds() - resetSearch() - }, [refetchPopularFeeds, setQuery, resetSearch]) - const onSubmitQuery = React.useCallback(() => { - debouncedSearch(query) - }, [query, debouncedSearch]) - const onPullToRefresh = React.useCallback(async () => { - setIsPTR(true) - await refetchPopularFeeds() - setIsPTR(false) - }, [setIsPTR, refetchPopularFeeds]) - const onEndReached = React.useCallback(() => { - if ( - isPopularFeedsFetching || - isUserSearching || - !hasNextPopularFeedsPage || - popularFeedsError + /** + * A search query is present. We may not have search results yet. + */ + const isUserSearching = query.length > 1 + const debouncedSearch = React.useMemo( + () => debounce(q => search(q), 500), // debounce for 500ms + [search], ) - return - fetchNextPopularFeedsPage() - }, [ - isPopularFeedsFetching, - isUserSearching, - popularFeedsError, - hasNextPopularFeedsPage, - fetchNextPopularFeedsPage, - ]) - - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + const onPressCompose = React.useCallback(() => { + openComposer({}) + }, [openComposer]) + const onChangeQuery = React.useCallback( + (text: string) => { + setQuery(text) + if (text.length > 1) { + debouncedSearch(text) + } else { + refetchPopularFeeds() + resetSearch() + } + }, + [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch], + ) + const onPressCancelSearch = React.useCallback(() => { + setQuery('') + refetchPopularFeeds() + resetSearch() + }, [refetchPopularFeeds, setQuery, resetSearch]) + const onSubmitQuery = React.useCallback(() => { + debouncedSearch(query) + }, [query, debouncedSearch]) + const onPullToRefresh = React.useCallback(async () => { + setIsPTR(true) + await refetchPopularFeeds() + setIsPTR(false) + }, [setIsPTR, refetchPopularFeeds]) + const onEndReached = React.useCallback(() => { + if ( + isPopularFeedsFetching || + isUserSearching || + !hasNextPopularFeedsPage || + popularFeedsError + ) + return + fetchNextPopularFeedsPage() + }, [ + isPopularFeedsFetching, + isUserSearching, + popularFeedsError, + hasNextPopularFeedsPage, + fetchNextPopularFeedsPage, + ]) - const items = React.useMemo(() => { - let slices: FlatlistSlice[] = [] + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - slices.push({ - key: 'savedFeedsHeader', - type: 'savedFeedsHeader', - }) + const items = React.useMemo(() => { + let slices: FlatlistSlice[] = [] - if (preferencesError) { slices.push({ - key: 'savedFeedsError', - type: 'error', - error: cleanError(preferencesError.toString()), + key: 'savedFeedsHeader', + type: 'savedFeedsHeader', }) - } else { - if (isPreferencesLoading || !preferences?.feeds?.saved) { + + if (preferencesError) { slices.push({ - key: 'savedFeedsLoading', - type: 'savedFeedsLoading', - // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, + key: 'savedFeedsError', + type: 'error', + error: cleanError(preferencesError.toString()), }) } else { - if (preferences?.feeds?.saved.length === 0) { + if (isPreferencesLoading || !preferences?.feeds?.saved) { slices.push({ - key: 'savedFeedNoResults', - type: 'savedFeedNoResults', + key: 'savedFeedsLoading', + type: 'savedFeedsLoading', + // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, }) } else { - const {saved, pinned} = preferences.feeds - - slices = slices.concat( - pinned.map(uri => ({ - key: `savedFeed:${uri}`, - type: 'savedFeed', - feedUri: uri, - })), - ) + if (preferences?.feeds?.saved.length === 0) { + slices.push({ + key: 'savedFeedNoResults', + type: 'savedFeedNoResults', + }) + } else { + const {saved, pinned} = preferences.feeds - slices = slices.concat( - saved - .filter(uri => !pinned.includes(uri)) - .map(uri => ({ + slices = slices.concat( + pinned.map(uri => ({ key: `savedFeed:${uri}`, type: 'savedFeed', feedUri: uri, })), - ) + ) + + slices = slices.concat( + saved + .filter(uri => !pinned.includes(uri)) + .map(uri => ({ + key: `savedFeed:${uri}`, + type: 'savedFeed', + feedUri: uri, + })), + ) + } } } - } - - slices.push({ - key: 'popularFeedsHeader', - type: 'popularFeedsHeader', - }) - if (popularFeedsError || searchError) { slices.push({ - key: 'popularFeedsError', - type: 'error', - error: cleanError( - popularFeedsError?.toString() ?? searchError?.toString() ?? '', - ), + key: 'popularFeedsHeader', + type: 'popularFeedsHeader', }) - } else { - if (isUserSearching) { - if (isSearchPending || !searchResults) { - slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', - }) - } else { - if (!searchResults || searchResults?.length === 0) { + + if (popularFeedsError || searchError) { + slices.push({ + key: 'popularFeedsError', + type: 'error', + error: cleanError( + popularFeedsError?.toString() ?? searchError?.toString() ?? '', + ), + }) + } else { + if (isUserSearching) { + if (isSearchPending || !searchResults) { slices.push({ - key: 'popularFeedsNoResults', - type: 'popularFeedsNoResults', + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', }) } else { - slices = slices.concat( - searchResults.map(feed => ({ - key: `popularFeed:${feed.uri}`, - type: 'popularFeed', - feedUri: feed.uri, - })), - ) + if (!searchResults || searchResults?.length === 0) { + slices.push({ + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', + }) + } else { + slices = slices.concat( + searchResults.map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + })), + ) + } } - } - } else { - if (isPopularFeedsFetching && !popularFeeds?.pages) { - slices.push({ - key: 'popularFeedsLoading', - type: 'popularFeedsLoading', - }) } else { - if ( - !popularFeeds?.pages || - popularFeeds?.pages[0]?.feeds?.length === 0 - ) { + if (isPopularFeedsFetching && !popularFeeds?.pages) { slices.push({ - key: 'popularFeedsNoResults', - type: 'popularFeedsNoResults', + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', }) } else { - for (const page of popularFeeds.pages || []) { - slices = slices.concat( - page.feeds - .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) - .map(feed => ({ - key: `popularFeed:${feed.uri}`, - type: 'popularFeed', - feedUri: feed.uri, - })), - ) - } - - if (isPopularFeedsFetchingNextPage) { + if ( + !popularFeeds?.pages || + popularFeeds?.pages[0]?.feeds?.length === 0 + ) { slices.push({ - key: 'popularFeedsLoadingMore', - type: 'popularFeedsLoadingMore', + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', }) + } else { + for (const page of popularFeeds.pages || []) { + slices = slices.concat( + page.feeds + .filter( + feed => !preferences?.feeds?.saved.includes(feed.uri), + ) + .map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + })), + ) + } + + if (isPopularFeedsFetchingNextPage) { + slices.push({ + key: 'popularFeedsLoadingMore', + type: 'popularFeedsLoadingMore', + }) + } } } } } - } - return slices - }, [ - preferences, - isPreferencesLoading, - preferencesError, - popularFeeds, - isPopularFeedsFetching, - popularFeedsError, - isPopularFeedsFetchingNextPage, - searchResults, - isSearchPending, - searchError, - isUserSearching, - ]) + return slices + }, [ + preferences, + isPreferencesLoading, + preferencesError, + popularFeeds, + isPopularFeedsFetching, + popularFeedsError, + isPopularFeedsFetchingNextPage, + searchResults, + isSearchPending, + searchError, + isUserSearching, + ]) - const renderHeaderBtn = React.useCallback(() => { - return ( - <Link - href="/settings/saved-feeds" - hitSlop={10} - accessibilityRole="button" - accessibilityLabel={_(msg`Edit Saved Feeds`)} - accessibilityHint="Opens screen to edit Saved Feeds"> - <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> - </Link> - ) - }, [pal, _]) + const renderHeaderBtn = React.useCallback(() => { + return ( + <Link + href="/settings/saved-feeds" + hitSlop={10} + accessibilityRole="button" + accessibilityLabel={_(msg`Edit Saved Feeds`)} + accessibilityHint="Opens screen to edit Saved Feeds"> + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> + </Link> + ) + }, [pal, _]) - const renderItem = React.useCallback( - ({item}: {item: FlatlistSlice}) => { - if (item.type === 'error') { - return <ErrorMessage message={item.error} /> - } else if ( - item.type === 'popularFeedsLoadingMore' || - item.type === 'savedFeedsLoading' - ) { - return ( - <View style={s.p10}> - <ActivityIndicator /> - </View> - ) - } else if (item.type === 'savedFeedsHeader') { - if (!isMobile) { + const renderItem = React.useCallback( + ({item}: {item: FlatlistSlice}) => { + if (item.type === 'error') { + return <ErrorMessage message={item.error} /> + } else if ( + item.type === 'popularFeedsLoadingMore' || + item.type === 'savedFeedsLoading' + ) { return ( - <View - style={[ - pal.view, - styles.header, - pal.border, - { - borderBottomWidth: 1, - }, - ]}> - <Text type="title-lg" style={[pal.text, s.bold]}> - <Trans>My Feeds</Trans> - </Text> - <Link - href="/settings/saved-feeds" - accessibilityLabel={_(msg`Edit My Feeds`)} - accessibilityHint=""> - <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> - </Link> + <View style={s.p10}> + <ActivityIndicator /> </View> ) - } - return <View /> - } else if (item.type === 'savedFeedNoResults') { - return ( - <View - style={{ - paddingHorizontal: 16, - paddingTop: 10, - }}> - <Text type="lg" style={pal.textLight}> - <Trans>You don't have any saved feeds!</Trans> - </Text> - </View> - ) - } else if (item.type === 'savedFeed') { - return <SavedFeed feedUri={item.feedUri} /> - } else if (item.type === 'popularFeedsHeader') { - return ( - <> + } else if (item.type === 'savedFeedsHeader') { + if (!isMobile) { + return ( + <View + style={[ + pal.view, + styles.header, + pal.border, + { + borderBottomWidth: 1, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.bold]}> + <Trans>My Feeds</Trans> + </Text> + <Link + href="/settings/saved-feeds" + accessibilityLabel={_(msg`Edit My Feeds`)} + accessibilityHint=""> + <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> + </Link> + </View> + ) + } + return <View /> + } else if (item.type === 'savedFeedNoResults') { + return ( <View - style={[ - pal.view, - styles.header, - { - marginTop: 16, - paddingLeft: isMobile ? 12 : undefined, - paddingRight: 10, - paddingBottom: isMobile ? 6 : undefined, - }, - ]}> - <Text type="title-lg" style={[pal.text, s.bold]}> - <Trans>Discover new feeds</Trans> + style={{ + paddingHorizontal: 16, + paddingTop: 10, + }}> + <Text type="lg" style={pal.textLight}> + <Trans>You don't have any saved feeds!</Trans> </Text> + </View> + ) + } else if (item.type === 'savedFeed') { + return <SavedFeed feedUri={item.feedUri} /> + } else if (item.type === 'popularFeedsHeader') { + return ( + <> + <View + style={[ + pal.view, + styles.header, + { + marginTop: 16, + paddingLeft: isMobile ? 12 : undefined, + paddingRight: 10, + paddingBottom: isMobile ? 6 : undefined, + }, + ]}> + <Text type="title-lg" style={[pal.text, s.bold]}> + <Trans>Discover new feeds</Trans> + </Text> + + {!isMobile && ( + <SearchInput + query={query} + onChangeQuery={onChangeQuery} + onPressCancelSearch={onPressCancelSearch} + onSubmitQuery={onSubmitQuery} + style={{flex: 1, maxWidth: 250}} + /> + )} + </View> - {!isMobile && ( - <SearchInput - query={query} - onChangeQuery={onChangeQuery} - onPressCancelSearch={onPressCancelSearch} - onSubmitQuery={onSubmitQuery} - style={{flex: 1, maxWidth: 250}} - /> + {isMobile && ( + <View style={{paddingHorizontal: 8, paddingBottom: 10}}> + <SearchInput + query={query} + onChangeQuery={onChangeQuery} + onPressCancelSearch={onPressCancelSearch} + onSubmitQuery={onSubmitQuery} + /> + </View> )} + </> + ) + } else if (item.type === 'popularFeedsLoading') { + return <FeedFeedLoadingPlaceholder /> + } else if (item.type === 'popularFeed') { + return ( + <FeedSourceCard + feedUri={item.feedUri} + showSaveBtn + showDescription + showLikes + /> + ) + } else if (item.type === 'popularFeedsNoResults') { + return ( + <View + style={{ + paddingHorizontal: 16, + paddingTop: 10, + paddingBottom: '150%', + }}> + <Text type="lg" style={pal.textLight}> + <Trans>No results found for "{query}"</Trans> + </Text> </View> + ) + } + return null + }, + [ + _, + isMobile, + pal, + query, + onChangeQuery, + onPressCancelSearch, + onSubmitQuery, + ], + ) - {isMobile && ( - <View style={{paddingHorizontal: 8, paddingBottom: 10}}> - <SearchInput - query={query} - onChangeQuery={onChangeQuery} - onPressCancelSearch={onPressCancelSearch} - onSubmitQuery={onSubmitQuery} - /> - </View> - )} - </> - ) - } else if (item.type === 'popularFeedsLoading') { - return <FeedFeedLoadingPlaceholder /> - } else if (item.type === 'popularFeed') { - return ( - <FeedSourceCard - feedUri={item.feedUri} - showSaveBtn - showDescription - showLikes + return ( + <View style={[pal.view, styles.container]}> + {isMobile && ( + <ViewHeader + title={_(msg`Feeds`)} + canGoBack={false} + renderButton={renderHeaderBtn} + showBorder /> - ) - } else if (item.type === 'popularFeedsNoResults') { - return ( - <View - style={{ - paddingHorizontal: 16, - paddingTop: 10, - paddingBottom: '150%', - }}> - <Text type="lg" style={pal.textLight}> - <Trans>No results found for "{query}"</Trans> - </Text> - </View> - ) - } - return null - }, - [ - _, - isMobile, - pal, - query, - onChangeQuery, - onPressCancelSearch, - onSubmitQuery, - ], - ) + )} - return ( - <View style={[pal.view, styles.container]}> - {isMobile && ( - <ViewHeader - title={_(msg`Feeds`)} - canGoBack={false} - renderButton={renderHeaderBtn} - showBorder - /> - )} - - {preferences ? <View /> : <ActivityIndicator />} + {preferences ? <View /> : <ActivityIndicator />} - <FlatList - style={[!isTabletOrDesktop && s.flex1, styles.list]} - data={items} - keyExtractor={item => item.key} - contentContainerStyle={styles.contentContainer} - renderItem={renderItem} - refreshControl={ - <RefreshControl - refreshing={isPTR} - onRefresh={isUserSearching ? undefined : onPullToRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - initialNumToRender={10} - onEndReached={onEndReached} - // @ts-ignore our .web version only -prf - desktopFixedHeight - /> + <FlatList + style={[!isTabletOrDesktop && s.flex1, styles.list]} + data={items} + keyExtractor={item => item.key} + contentContainerStyle={styles.contentContainer} + renderItem={renderItem} + refreshControl={ + <RefreshControl + refreshing={isPTR} + onRefresh={isUserSearching ? undefined : onPullToRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + initialNumToRender={10} + onEndReached={onEndReached} + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> - <FAB - testID="composeFAB" - onPress={onPressCompose} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - </View> - ) -}) + <FAB + testID="composeFAB" + onPress={onPressCompose} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> + </View> + ) + }, + {isPublic: true}, +) function SavedFeed({feedUri}: {feedUri: string}) { const pal = usePalette('default') diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 18a0cbc15..8df945cd2 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -14,22 +14,56 @@ import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' import {usePreferencesQuery} from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {emitSoftReset} from '#/state/events' +import {useSession} from '#/state/session' type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> -export const HomeScreen = withAuthRequired(function HomeScreenImpl( - props: Props, -) { - const {data: preferences} = usePreferencesQuery() - if (preferences) { - return <HomeScreenReady {...props} preferences={preferences} /> - } else { - return ( - <View style={styles.loading}> - <ActivityIndicator size="large" /> - </View> - ) - } -}) +export const HomeScreen = withAuthRequired( + function HomeScreenImpl(props: Props) { + const {hasSession} = useSession() + const {data: preferences} = usePreferencesQuery() + + if (!hasSession) { + return <HomeScreenPublic /> + } + + if (preferences) { + return <HomeScreenReady {...props} preferences={preferences} /> + } else { + return ( + <View style={styles.loading}> + <ActivityIndicator size="large" /> + </View> + ) + } + }, + { + isPublic: true, + }, +) + +function HomeScreenPublic() { + const setMinimalShellMode = useSetMinimalShellMode() + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() + + const renderCustomFeedEmptyState = React.useCallback(() => { + return <CustomFeedEmptyState /> + }, []) + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + setDrawerSwipeDisabled(false) + }, [setDrawerSwipeDisabled, setMinimalShellMode]), + ) + + return ( + <FeedPage + isPageFocused + feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`} + renderEmptyState={renderCustomFeedEmptyState} + /> + ) +} function HomeScreenReady({ preferences, @@ -83,6 +117,7 @@ function HomeScreenReady({ emitSoftReset() }, []) + // TODO(pwi) may need this in public view const onPageScrollStateChanged = React.useCallback( (state: 'idle' | 'dragging' | 'settling') => { if (state === 'dragging') { diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx index ab7bbcefe..2209310d0 100644 --- a/src/view/screens/PostLikedBy.tsx +++ b/src/view/screens/PostLikedBy.tsx @@ -11,22 +11,25 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> -export const PostLikedByScreen = withAuthRequired(({route}: Props) => { - const setMinimalShellMode = useSetMinimalShellMode() - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {_} = useLingui() +export const PostLikedByScreen = withAuthRequired( + ({route}: Props) => { + const setMinimalShellMode = useSetMinimalShellMode() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Liked by`)} /> - <PostLikedByComponent uri={uri} /> - </View> - ) -}) + return ( + <View> + <ViewHeader title={_(msg`Liked by`)} /> + <PostLikedByComponent uri={uri} /> + </View> + ) + }, + {isPublic: true}, +) diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx index eabbc4a4e..5b3b5f8fa 100644 --- a/src/view/screens/PostRepostedBy.tsx +++ b/src/view/screens/PostRepostedBy.tsx @@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> -export const PostRepostedByScreen = withAuthRequired(({route}: Props) => { - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const PostRepostedByScreen = withAuthRequired( + ({route}: Props) => { + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Reposted by`)} /> - <PostRepostedByComponent uri={uri} /> - </View> - ) -}) + return ( + <View> + <ViewHeader title={_(msg`Reposted by`)} /> + <PostRepostedByComponent uri={uri} /> + </View> + ) + }, + {isPublic: true}, +) diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index 0476e182b..11574e283 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -27,84 +27,85 @@ import {CenteredView} from '../com/util/Views' import {useComposerControls} from '#/state/shell/composer' type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> -export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({ - route, -}: Props) { - const queryClient = useQueryClient() - const {_} = useLingui() - const {fabMinimalShellTransform} = useMinimalShellMode() - const setMinimalShellMode = useSetMinimalShellMode() - const {openComposer} = useComposerControls() - const safeAreaInsets = useSafeAreaInsets() - const {name, rkey} = route.params - const {isMobile} = useWebMediaQueries() - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) - const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) +export const PostThreadScreen = withAuthRequired( + function PostThreadScreenImpl({route}: Props) { + const queryClient = useQueryClient() + const {_} = useLingui() + const {fabMinimalShellTransform} = useMinimalShellMode() + const setMinimalShellMode = useSetMinimalShellMode() + const {openComposer} = useComposerControls() + const safeAreaInsets = useSafeAreaInsets() + const {name, rkey} = route.params + const {isMobile} = useWebMediaQueries() + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) + const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) - - const onPressReply = React.useCallback(() => { - if (!resolvedUri) { - return - } - const thread = queryClient.getQueryData<ThreadNode>( - POST_THREAD_RQKEY(resolvedUri.uri), + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), ) - if (thread?.type !== 'post') { - return - } - openComposer({ - replyTo: { - uri: thread.post.uri, - cid: thread.post.cid, - text: thread.record.text, - author: { - handle: thread.post.author.handle, - displayName: thread.post.author.displayName, - avatar: thread.post.author.avatar, + + const onPressReply = React.useCallback(() => { + if (!resolvedUri) { + return + } + const thread = queryClient.getQueryData<ThreadNode>( + POST_THREAD_RQKEY(resolvedUri.uri), + ) + if (thread?.type !== 'post') { + return + } + openComposer({ + replyTo: { + uri: thread.post.uri, + cid: thread.post.cid, + text: thread.record.text, + author: { + handle: thread.post.author.handle, + displayName: thread.post.author.displayName, + avatar: thread.post.author.avatar, + }, }, - }, - onPost: () => - queryClient.invalidateQueries({ - queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), - }), - }) - }, [openComposer, queryClient, resolvedUri]) + onPost: () => + queryClient.invalidateQueries({ + queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), + }), + }) + }, [openComposer, queryClient, resolvedUri]) - return ( - <View style={s.hContentRegion}> - {isMobile && <ViewHeader title={_(msg`Post`)} />} - <View style={s.flex1}> - {uriError ? ( - <CenteredView> - <ErrorMessage message={String(uriError)} /> - </CenteredView> - ) : ( - <PostThreadComponent - uri={resolvedUri?.uri} - onPressReply={onPressReply} - /> + return ( + <View style={s.hContentRegion}> + {isMobile && <ViewHeader title={_(msg`Post`)} />} + <View style={s.flex1}> + {uriError ? ( + <CenteredView> + <ErrorMessage message={String(uriError)} /> + </CenteredView> + ) : ( + <PostThreadComponent + uri={resolvedUri?.uri} + onPressReply={onPressReply} + /> + )} + </View> + {isMobile && ( + <Animated.View + style={[ + styles.prompt, + fabMinimalShellTransform, + { + bottom: clamp(safeAreaInsets.bottom, 15, 30), + }, + ]}> + <ComposePrompt onPressCompose={onPressReply} /> + </Animated.View> )} </View> - {isMobile && ( - <Animated.View - style={[ - styles.prompt, - fabMinimalShellTransform, - { - bottom: clamp(safeAreaInsets.bottom, 15, 30), - }, - ]}> - <ComposePrompt onPressCompose={onPressReply} /> - </Animated.View> - )} - </View> - ) -}) + ) + }, + {isPublic: true}, +) const styles = StyleSheet.create({ prompt: { diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 88b11b114..5411bc044 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -43,82 +43,85 @@ interface SectionRef { } type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> -export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({ - route, -}: Props) { - const {currentAccount} = useSession() - const name = - route.params.name === 'me' ? currentAccount?.did : route.params.name - const moderationOpts = useModerationOpts() - const { - data: resolvedDid, - error: resolveError, - refetch: refetchDid, - isFetching: isFetchingDid, - } = useResolveDidQuery(name) - const { - data: profile, - dataUpdatedAt, - error: profileError, - refetch: refetchProfile, - isFetching: isFetchingProfile, - } = useProfileQuery({ - did: resolvedDid?.did, - }) +export const ProfileScreen = withAuthRequired( + function ProfileScreenImpl({route}: Props) { + const {currentAccount} = useSession() + const name = + route.params.name === 'me' ? currentAccount?.did : route.params.name + const moderationOpts = useModerationOpts() + const { + data: resolvedDid, + error: resolveError, + refetch: refetchDid, + isFetching: isFetchingDid, + } = useResolveDidQuery(name) + const { + data: profile, + dataUpdatedAt, + error: profileError, + refetch: refetchProfile, + isFetching: isFetchingProfile, + } = useProfileQuery({ + did: resolvedDid?.did, + }) - const onPressTryAgain = React.useCallback(() => { - if (resolveError) { - refetchDid() - } else { - refetchProfile() - } - }, [resolveError, refetchDid, refetchProfile]) + const onPressTryAgain = React.useCallback(() => { + if (resolveError) { + refetchDid() + } else { + refetchProfile() + } + }, [resolveError, refetchDid, refetchProfile]) - if (isFetchingDid || isFetchingProfile || !moderationOpts) { - return ( - <CenteredView> - <ProfileHeader - profile={null} - moderation={null} - isProfilePreview={true} + if (isFetchingDid || isFetchingProfile || !moderationOpts) { + return ( + <CenteredView> + <ProfileHeader + profile={null} + moderation={null} + isProfilePreview={true} + /> + </CenteredView> + ) + } + if (resolveError || profileError) { + return ( + <CenteredView> + <ErrorScreen + testID="profileErrorScreen" + title="Oops!" + message={cleanError(resolveError || profileError)} + onPressTryAgain={onPressTryAgain} + /> + </CenteredView> + ) + } + if (profile && moderationOpts) { + return ( + <ProfileScreenLoaded + profile={profile} + dataUpdatedAt={dataUpdatedAt} + moderationOpts={moderationOpts} + hideBackButton={!!route.params.hideBackButton} /> - </CenteredView> - ) - } - if (resolveError || profileError) { + ) + } + // should never happen return ( <CenteredView> <ErrorScreen testID="profileErrorScreen" title="Oops!" - message={cleanError(resolveError || profileError)} + message="Something went wrong and we're not sure what." onPressTryAgain={onPressTryAgain} /> </CenteredView> ) - } - if (profile && moderationOpts) { - return ( - <ProfileScreenLoaded - profile={profile} - dataUpdatedAt={dataUpdatedAt} - moderationOpts={moderationOpts} - hideBackButton={!!route.params.hideBackButton} - /> - ) - } - // should never happen - return ( - <CenteredView> - <ErrorScreen - testID="profileErrorScreen" - title="Oops!" - message="Something went wrong and we're not sure what." - onPressTryAgain={onPressTryAgain} - /> - </CenteredView> - ) -}) + }, + { + isPublic: true, + }, +) function ProfileScreenLoaded({ profile: profileUnshadowed, diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 3974d3a11..2cdcdb3b5 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -129,6 +129,9 @@ export const ProfileFeedScreen = withAuthRequired( </CenteredView> ) }, + { + isPublic: true, + }, ) function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) { diff --git a/src/view/screens/ProfileFeedLikedBy.tsx b/src/view/screens/ProfileFeedLikedBy.tsx index c8466360e..6399c8a0b 100644 --- a/src/view/screens/ProfileFeedLikedBy.tsx +++ b/src/view/screens/ProfileFeedLikedBy.tsx @@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'> -export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => { - const setMinimalShellMode = useSetMinimalShellMode() - const {name, rkey} = route.params - const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) - const {_} = useLingui() +export const ProfileFeedLikedByScreen = withAuthRequired( + ({route}: Props) => { + const setMinimalShellMode = useSetMinimalShellMode() + const {name, rkey} = route.params + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Liked by`)} /> - <PostLikedByComponent uri={uri} /> - </View> - ) -}) + return ( + <View> + <ViewHeader title={_(msg`Liked by`)} /> + <PostLikedByComponent uri={uri} /> + </View> + ) + }, + {isPublic: true}, +) diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx index 13e69541a..71c0e4a9c 100644 --- a/src/view/screens/ProfileFollowers.tsx +++ b/src/view/screens/ProfileFollowers.tsx @@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> -export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => { - const {name} = route.params - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const ProfileFollowersScreen = withAuthRequired( + ({route}: Props) => { + const {name} = route.params + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Followers`)} /> - <ProfileFollowersComponent name={name} /> - </View> - ) -}) + return ( + <View> + <ViewHeader title={_(msg`Followers`)} /> + <ProfileFollowersComponent name={name} /> + </View> + ) + }, + {isPublic: true}, +) diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx index 07d6eaa78..bb3f2040f 100644 --- a/src/view/screens/ProfileFollows.tsx +++ b/src/view/screens/ProfileFollows.tsx @@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react' import {msg} from '@lingui/macro' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> -export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => { - const {name} = route.params - const setMinimalShellMode = useSetMinimalShellMode() - const {_} = useLingui() +export const ProfileFollowsScreen = withAuthRequired( + ({route}: Props) => { + const {name} = route.params + const setMinimalShellMode = useSetMinimalShellMode() + const {_} = useLingui() - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - }, [setMinimalShellMode]), - ) + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) - return ( - <View> - <ViewHeader title={_(msg`Following`)} /> - <ProfileFollowsComponent name={name} /> - </View> - ) -}) + return ( + <View> + <ViewHeader title={_(msg`Following`)} /> + <ProfileFollowsComponent name={name} /> + </View> + ) + }, + {isPublic: true}, +) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 4928b6745..4c13a2be1 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -33,7 +33,7 @@ import { usePinFeedMutation, useUnpinFeedMutation, useSetSaveFeedsMutation, - usePreferencesQueryKey, + preferencesQueryKey, UsePreferencesQueryResponse, } from '#/state/queries/preferences' @@ -182,9 +182,10 @@ function ListItem({ const onPressUp = React.useCallback(async () => { if (!isPinned) return - const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>( - usePreferencesQueryKey, - )?.feeds + const feeds = + queryClient.getQueryData<UsePreferencesQueryResponse>( + preferencesQueryKey, + )?.feeds const pinned = feeds?.pinned ?? [] const index = pinned.indexOf(feedUri) @@ -206,9 +207,10 @@ function ListItem({ const onPressDown = React.useCallback(async () => { if (!isPinned) return - const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>( - usePreferencesQueryKey, - )?.feeds + const feeds = + queryClient.getQueryData<UsePreferencesQueryResponse>( + preferencesQueryKey, + )?.feeds const pinned = feeds?.pinned ?? [] const index = pinned.indexOf(feedUri) |