Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

PWI Base (#1964)

* Base work for public view

* Make default moderation settings more restrictive

* Fix type

* Handle showing sign-in on authed actions

* Fix hoc logic

* Simplify prefs logic

* Remove duplicate method

* Add todo

* Clean up RepostButton.web

* Fix x button color

* Add todo

* Retain existing label prefs for now, use separate logged out settings

* Clean up useAuthedMethod, rename to useRequireAuth

* Add todos

* Move dismiss logic to withAuthRequired

* Ooops add web

* Block public view in prod

* Add todo

* Fix bad import

authored by

Eric Bailey and committed by
GitHub
f18b9b32 71b59021

+1022 -751
+24
src/state/queries/preferences/const.ts
··· 2 2 UsePreferencesQueryResponse, 3 3 ThreadViewPreferences, 4 4 } from '#/state/queries/preferences/types' 5 + import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation' 5 6 6 7 export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] = 7 8 { ··· 25 26 pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')], 26 27 saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')], 27 28 } 29 + 30 + export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = { 31 + birthDate: new Date('2022-11-17'), // TODO(pwi) 32 + adultContentEnabled: false, 33 + feeds: { 34 + saved: [], 35 + pinned: [], 36 + unpinned: [], 37 + }, 38 + // labels are undefined until set by user 39 + contentLabels: { 40 + nsfw: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nsfw, 41 + nudity: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nudity, 42 + suggestive: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.suggestive, 43 + gore: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.gore, 44 + hate: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.hate, 45 + spam: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.spam, 46 + impersonation: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.impersonation, 47 + }, 48 + feedViewPrefs: DEFAULT_HOME_FEED_PREFS, 49 + threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS, 50 + userAge: 13, // TODO(pwi) 51 + }
+67 -62
src/state/queries/preferences/index.ts
··· 15 15 import { 16 16 DEFAULT_HOME_FEED_PREFS, 17 17 DEFAULT_THREAD_VIEW_PREFS, 18 + DEFAULT_LOGGED_OUT_PREFERENCES, 18 19 } from '#/state/queries/preferences/const' 19 20 import {getModerationOpts} from '#/state/queries/preferences/moderation' 20 21 import {STALE} from '#/state/queries' ··· 23 24 export * from '#/state/queries/preferences/moderation' 24 25 export * from '#/state/queries/preferences/const' 25 26 26 - export const usePreferencesQueryKey = ['getPreferences'] 27 + export const preferencesQueryKey = ['getPreferences'] 27 28 28 29 export function usePreferencesQuery() { 29 - const {hasSession} = useSession() 30 30 return useQuery({ 31 - enabled: hasSession, 32 31 staleTime: STALE.MINUTES.ONE, 33 - queryKey: usePreferencesQueryKey, 32 + queryKey: preferencesQueryKey, 34 33 queryFn: async () => { 35 - const res = await getAgent().getPreferences() 36 - const preferences: UsePreferencesQueryResponse = { 37 - ...res, 38 - feeds: { 39 - saved: res.feeds?.saved || [], 40 - pinned: res.feeds?.pinned || [], 41 - unpinned: 42 - res.feeds.saved?.filter(f => { 43 - return !res.feeds.pinned?.includes(f) 44 - }) || [], 45 - }, 46 - // labels are undefined until set by user 47 - contentLabels: { 48 - nsfw: temp__migrateLabelPref( 49 - res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw, 50 - ), 51 - nudity: temp__migrateLabelPref( 52 - res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity, 53 - ), 54 - suggestive: temp__migrateLabelPref( 55 - res.contentLabels?.suggestive || 56 - DEFAULT_LABEL_PREFERENCES.suggestive, 57 - ), 58 - gore: temp__migrateLabelPref( 59 - res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore, 60 - ), 61 - hate: temp__migrateLabelPref( 62 - res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate, 63 - ), 64 - spam: temp__migrateLabelPref( 65 - res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam, 66 - ), 67 - impersonation: temp__migrateLabelPref( 68 - res.contentLabels?.impersonation || 69 - DEFAULT_LABEL_PREFERENCES.impersonation, 70 - ), 71 - }, 72 - feedViewPrefs: { 73 - ...DEFAULT_HOME_FEED_PREFS, 74 - ...(res.feedViewPrefs.home || {}), 75 - }, 76 - threadViewPrefs: { 77 - ...DEFAULT_THREAD_VIEW_PREFS, 78 - ...(res.threadViewPrefs ?? {}), 79 - }, 80 - userAge: res.birthDate ? getAge(res.birthDate) : undefined, 34 + const agent = getAgent() 35 + 36 + if (agent.session?.did === undefined) { 37 + return DEFAULT_LOGGED_OUT_PREFERENCES 38 + } else { 39 + const res = await agent.getPreferences() 40 + const preferences: UsePreferencesQueryResponse = { 41 + ...res, 42 + feeds: { 43 + saved: res.feeds?.saved || [], 44 + pinned: res.feeds?.pinned || [], 45 + unpinned: 46 + res.feeds.saved?.filter(f => { 47 + return !res.feeds.pinned?.includes(f) 48 + }) || [], 49 + }, 50 + // labels are undefined until set by user 51 + contentLabels: { 52 + nsfw: temp__migrateLabelPref( 53 + res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw, 54 + ), 55 + nudity: temp__migrateLabelPref( 56 + res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity, 57 + ), 58 + suggestive: temp__migrateLabelPref( 59 + res.contentLabels?.suggestive || 60 + DEFAULT_LABEL_PREFERENCES.suggestive, 61 + ), 62 + gore: temp__migrateLabelPref( 63 + res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore, 64 + ), 65 + hate: temp__migrateLabelPref( 66 + res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate, 67 + ), 68 + spam: temp__migrateLabelPref( 69 + res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam, 70 + ), 71 + impersonation: temp__migrateLabelPref( 72 + res.contentLabels?.impersonation || 73 + DEFAULT_LABEL_PREFERENCES.impersonation, 74 + ), 75 + }, 76 + feedViewPrefs: { 77 + ...DEFAULT_HOME_FEED_PREFS, 78 + ...(res.feedViewPrefs.home || {}), 79 + }, 80 + threadViewPrefs: { 81 + ...DEFAULT_THREAD_VIEW_PREFS, 82 + ...(res.threadViewPrefs ?? {}), 83 + }, 84 + userAge: res.birthDate ? getAge(res.birthDate) : undefined, 85 + } 86 + return preferences 81 87 } 82 - return preferences 83 88 }, 84 89 }) 85 90 } ··· 107 112 await getAgent().app.bsky.actor.putPreferences({preferences: []}) 108 113 // triggers a refetch 109 114 await queryClient.invalidateQueries({ 110 - queryKey: usePreferencesQueryKey, 115 + queryKey: preferencesQueryKey, 111 116 }) 112 117 }, 113 118 }) ··· 125 130 await getAgent().setContentLabelPref(labelGroup, visibility) 126 131 // triggers a refetch 127 132 await queryClient.invalidateQueries({ 128 - queryKey: usePreferencesQueryKey, 133 + queryKey: preferencesQueryKey, 129 134 }) 130 135 }, 131 136 }) ··· 139 144 await getAgent().setAdultContentEnabled(enabled) 140 145 // triggers a refetch 141 146 await queryClient.invalidateQueries({ 142 - queryKey: usePreferencesQueryKey, 147 + queryKey: preferencesQueryKey, 143 148 }) 144 149 }, 145 150 }) ··· 153 158 await getAgent().setPersonalDetails({birthDate}) 154 159 // triggers a refetch 155 160 await queryClient.invalidateQueries({ 156 - queryKey: usePreferencesQueryKey, 161 + queryKey: preferencesQueryKey, 157 162 }) 158 163 }, 159 164 }) ··· 167 172 await getAgent().setFeedViewPrefs('home', prefs) 168 173 // triggers a refetch 169 174 await queryClient.invalidateQueries({ 170 - queryKey: usePreferencesQueryKey, 175 + queryKey: preferencesQueryKey, 171 176 }) 172 177 }, 173 178 }) ··· 181 186 await getAgent().setThreadViewPrefs(prefs) 182 187 // triggers a refetch 183 188 await queryClient.invalidateQueries({ 184 - queryKey: usePreferencesQueryKey, 189 + queryKey: preferencesQueryKey, 185 190 }) 186 191 }, 187 192 }) ··· 199 204 await getAgent().setSavedFeeds(saved, pinned) 200 205 // triggers a refetch 201 206 await queryClient.invalidateQueries({ 202 - queryKey: usePreferencesQueryKey, 207 + queryKey: preferencesQueryKey, 203 208 }) 204 209 }, 205 210 }) ··· 214 219 track('CustomFeed:Save') 215 220 // triggers a refetch 216 221 await queryClient.invalidateQueries({ 217 - queryKey: usePreferencesQueryKey, 222 + queryKey: preferencesQueryKey, 218 223 }) 219 224 }, 220 225 }) ··· 229 234 track('CustomFeed:Unsave') 230 235 // triggers a refetch 231 236 await queryClient.invalidateQueries({ 232 - queryKey: usePreferencesQueryKey, 237 + queryKey: preferencesQueryKey, 233 238 }) 234 239 }, 235 240 }) ··· 244 249 track('CustomFeed:Pin', {uri}) 245 250 // triggers a refetch 246 251 await queryClient.invalidateQueries({ 247 - queryKey: usePreferencesQueryKey, 252 + queryKey: preferencesQueryKey, 248 253 }) 249 254 }, 250 255 }) ··· 259 264 track('CustomFeed:Unpin', {uri}) 260 265 // triggers a refetch 261 266 await queryClient.invalidateQueries({ 262 - queryKey: usePreferencesQueryKey, 267 + queryKey: preferencesQueryKey, 263 268 }) 264 269 }, 265 270 })
+18
src/state/queries/preferences/moderation.ts
··· 34 34 impersonation: 'hide', 35 35 } 36 36 37 + /** 38 + * More strict than our default settings for logged in users. 39 + * 40 + * TODO(pwi) 41 + */ 42 + export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: Record< 43 + ConfigurableLabelGroup, 44 + LabelPreference 45 + > = { 46 + nsfw: 'hide', 47 + nudity: 'hide', 48 + suggestive: 'hide', 49 + gore: 'hide', 50 + hate: 'hide', 51 + spam: 'hide', 52 + impersonation: 'hide', 53 + } 54 + 37 55 export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = { 38 56 id: 'illegal', 39 57 title: 'Illegal Content',
+4 -1
src/state/queries/preferences/types.ts
··· 43 43 } 44 44 } 45 45 46 - export type ThreadViewPreferences = Omit<BskyThreadViewPreference, 'sort'> & { 46 + export type ThreadViewPreferences = Pick< 47 + BskyThreadViewPreference, 48 + 'prioritizeFollowedUsers' 49 + > & { 47 50 sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string 48 51 lab_treeViewEnabled?: boolean 49 52 }
+17
src/state/session/index.tsx
··· 8 8 import {PUBLIC_BSKY_AGENT} from '#/state/queries' 9 9 import {IS_PROD} from '#/lib/constants' 10 10 import {emitSessionLoaded, emitSessionDropped} from '../events' 11 + import {useLoggedOutViewControls} from '#/state/shell/logged-out' 11 12 12 13 let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT 13 14 ··· 515 516 export function useSessionApi() { 516 517 return React.useContext(ApiContext) 517 518 } 519 + 520 + export function useRequireAuth() { 521 + const {hasSession} = useSession() 522 + const {setShowLoggedOut} = useLoggedOutViewControls() 523 + 524 + return React.useCallback( 525 + (fn: () => void) => { 526 + if (hasSession) { 527 + fn() 528 + } else { 529 + setShowLoggedOut(true) 530 + } 531 + }, 532 + [hasSession, setShowLoggedOut], 533 + ) 534 + }
+18 -13
src/state/shell/index.tsx
··· 7 7 import {Provider as OnboardingProvider} from './onboarding' 8 8 import {Provider as ComposerProvider} from './composer' 9 9 import {Provider as TickEveryMinuteProvider} from './tick-every-minute' 10 + import {Provider as LoggedOutViewProvider} from './logged-out' 10 11 11 12 export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' 12 13 export { ··· 22 23 export function Provider({children}: React.PropsWithChildren<{}>) { 23 24 return ( 24 25 <ShellLayoutProvder> 25 - <DrawerOpenProvider> 26 - <DrawerSwipableProvider> 27 - <MinimalModeProvider> 28 - <ColorModeProvider> 29 - <OnboardingProvider> 30 - <ComposerProvider> 31 - <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> 32 - </ComposerProvider> 33 - </OnboardingProvider> 34 - </ColorModeProvider> 35 - </MinimalModeProvider> 36 - </DrawerSwipableProvider> 37 - </DrawerOpenProvider> 26 + <LoggedOutViewProvider> 27 + <DrawerOpenProvider> 28 + <DrawerSwipableProvider> 29 + <MinimalModeProvider> 30 + <ColorModeProvider> 31 + <OnboardingProvider> 32 + <ComposerProvider> 33 + <TickEveryMinuteProvider> 34 + {children} 35 + </TickEveryMinuteProvider> 36 + </ComposerProvider> 37 + </OnboardingProvider> 38 + </ColorModeProvider> 39 + </MinimalModeProvider> 40 + </DrawerSwipableProvider> 41 + </DrawerOpenProvider> 42 + </LoggedOutViewProvider> 38 43 </ShellLayoutProvder> 39 44 ) 40 45 }
+37
src/state/shell/logged-out.tsx
··· 1 + import React from 'react' 2 + 3 + type StateContext = { 4 + showLoggedOut: boolean 5 + } 6 + 7 + const StateContext = React.createContext<StateContext>({ 8 + showLoggedOut: false, 9 + }) 10 + const ControlsContext = React.createContext<{ 11 + setShowLoggedOut: (show: boolean) => void 12 + }>({ 13 + setShowLoggedOut: () => {}, 14 + }) 15 + 16 + export function Provider({children}: React.PropsWithChildren<{}>) { 17 + const [showLoggedOut, setShowLoggedOut] = React.useState(false) 18 + 19 + const state = React.useMemo(() => ({showLoggedOut}), [showLoggedOut]) 20 + const controls = React.useMemo(() => ({setShowLoggedOut}), [setShowLoggedOut]) 21 + 22 + return ( 23 + <StateContext.Provider value={state}> 24 + <ControlsContext.Provider value={controls}> 25 + {children} 26 + </ControlsContext.Provider> 27 + </StateContext.Provider> 28 + ) 29 + } 30 + 31 + export function useLoggedOutView() { 32 + return React.useContext(StateContext) 33 + } 34 + 35 + export function useLoggedOutViewControls() { 36 + return React.useContext(ControlsContext) 37 + }
+2 -1
src/view/com/auth/LoggedOut.tsx
··· 15 15 S_CreateAccount, 16 16 } 17 17 18 - export function LoggedOut() { 18 + export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { 19 19 const pal = usePalette('default') 20 20 const setMinimalShellMode = useSetMinimalShellMode() 21 21 const {screen} = useAnalytics() ··· 31 31 if (screenState === ScreenState.S_LoginOrCreateAccount) { 32 32 return ( 33 33 <SplashScreen 34 + onDismiss={onDismiss} 34 35 onPressSignin={() => setScreenState(ScreenState.S_Login)} 35 36 onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)} 36 37 />
+31 -1
src/view/com/auth/SplashScreen.tsx
··· 1 1 import React from 'react' 2 - import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import { 3 + SafeAreaView, 4 + StyleSheet, 5 + TouchableOpacity, 6 + Pressable, 7 + View, 8 + } from 'react-native' 9 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 3 10 import {Text} from 'view/com/util/text/Text' 4 11 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' 5 12 import {s, colors} from 'lib/styles' ··· 9 16 import {useLingui} from '@lingui/react' 10 17 11 18 export const SplashScreen = ({ 19 + onDismiss, 12 20 onPressSignin, 13 21 onPressCreateAccount, 14 22 }: { 23 + onDismiss?: () => void 15 24 onPressSignin: () => void 16 25 onPressCreateAccount: () => void 17 26 }) => { ··· 20 29 21 30 return ( 22 31 <CenteredView style={[styles.container, pal.view]}> 32 + {onDismiss && ( 33 + <Pressable 34 + accessibilityRole="button" 35 + style={{ 36 + position: 'absolute', 37 + top: 20, 38 + right: 20, 39 + padding: 20, 40 + zIndex: 100, 41 + }} 42 + onPress={onDismiss}> 43 + <FontAwesomeIcon 44 + icon="x" 45 + size={24} 46 + style={{ 47 + color: String(pal.text.color), 48 + }} 49 + /> 50 + </Pressable> 51 + )} 52 + 23 53 <SafeAreaView testID="noSessionView" style={styles.container}> 24 54 <ErrorBoundary> 25 55 <View style={styles.hero}>
+68 -42
src/view/com/auth/SplashScreen.web.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, TouchableOpacity, View} from 'react-native' 2 + import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native' 3 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 3 4 import {Text} from 'view/com/util/text/Text' 4 5 import {TextLink} from '../util/Link' 5 6 import {ErrorBoundary} from 'view/com/util/ErrorBoundary' ··· 11 12 import {Trans} from '@lingui/macro' 12 13 13 14 export const SplashScreen = ({ 15 + onDismiss, 14 16 onPressSignin, 15 17 onPressCreateAccount, 16 18 }: { 19 + onDismiss?: () => void 17 20 onPressSignin: () => void 18 21 onPressCreateAccount: () => void 19 22 }) => { ··· 23 26 const isMobileWeb = isWeb && isTabletOrMobile 24 27 25 28 return ( 26 - <CenteredView style={[styles.container, pal.view]}> 27 - <View 28 - testID="noSessionView" 29 - style={[ 30 - styles.containerInner, 31 - isMobileWeb && styles.containerInnerMobile, 32 - pal.border, 33 - ]}> 34 - <ErrorBoundary> 35 - <Text style={isMobileWeb ? styles.titleMobile : styles.title}> 36 - Bluesky 37 - </Text> 38 - <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}> 39 - See what's next 40 - </Text> 41 - <View testID="signinOrCreateAccount" style={styles.btns}> 42 - <TouchableOpacity 43 - testID="createAccountButton" 44 - style={[styles.btn, {backgroundColor: colors.blue3}]} 45 - onPress={onPressCreateAccount} 46 - // TODO: web accessibility 47 - accessibilityRole="button"> 48 - <Text style={[s.white, styles.btnLabel]}> 49 - Create a new account 50 - </Text> 51 - </TouchableOpacity> 52 - <TouchableOpacity 53 - testID="signInButton" 54 - style={[styles.btn, pal.btn]} 55 - onPress={onPressSignin} 56 - // TODO: web accessibility 57 - accessibilityRole="button"> 58 - <Text style={[pal.text, styles.btnLabel]}> 59 - <Trans>Sign In</Trans> 60 - </Text> 61 - </TouchableOpacity> 62 - </View> 63 - </ErrorBoundary> 64 - </View> 65 - <Footer styles={styles} /> 66 - </CenteredView> 29 + <> 30 + {onDismiss && ( 31 + <Pressable 32 + accessibilityRole="button" 33 + style={{ 34 + position: 'absolute', 35 + top: 20, 36 + right: 20, 37 + padding: 20, 38 + zIndex: 100, 39 + }} 40 + onPress={onDismiss}> 41 + <FontAwesomeIcon 42 + icon="x" 43 + size={24} 44 + style={{ 45 + color: String(pal.text.color), 46 + }} 47 + /> 48 + </Pressable> 49 + )} 50 + 51 + <CenteredView style={[styles.container, pal.view]}> 52 + <View 53 + testID="noSessionView" 54 + style={[ 55 + styles.containerInner, 56 + isMobileWeb && styles.containerInnerMobile, 57 + pal.border, 58 + ]}> 59 + <ErrorBoundary> 60 + <Text style={isMobileWeb ? styles.titleMobile : styles.title}> 61 + Bluesky 62 + </Text> 63 + <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}> 64 + See what's next 65 + </Text> 66 + <View testID="signinOrCreateAccount" style={styles.btns}> 67 + <TouchableOpacity 68 + testID="createAccountButton" 69 + style={[styles.btn, {backgroundColor: colors.blue3}]} 70 + onPress={onPressCreateAccount} 71 + // TODO: web accessibility 72 + accessibilityRole="button"> 73 + <Text style={[s.white, styles.btnLabel]}> 74 + Create a new account 75 + </Text> 76 + </TouchableOpacity> 77 + <TouchableOpacity 78 + testID="signInButton" 79 + style={[styles.btn, pal.btn]} 80 + onPress={onPressSignin} 81 + // TODO: web accessibility 82 + accessibilityRole="button"> 83 + <Text style={[pal.text, styles.btnLabel]}> 84 + <Trans>Sign In</Trans> 85 + </Text> 86 + </TouchableOpacity> 87 + </View> 88 + </ErrorBoundary> 89 + </View> 90 + <Footer styles={styles} /> 91 + </CenteredView> 92 + </> 67 93 ) 68 94 } 69 95
+16 -1
src/view/com/auth/withAuthRequired.tsx
··· 13 13 import {STATUS_PAGE_URL} from 'lib/constants' 14 14 import {useOnboardingState} from '#/state/shell' 15 15 import {useSession} from '#/state/session' 16 + import { 17 + useLoggedOutView, 18 + useLoggedOutViewControls, 19 + } from '#/state/shell/logged-out' 20 + import {IS_PROD} from '#/env' 16 21 17 22 export const withAuthRequired = <P extends object>( 18 23 Component: React.ComponentType<P>, 24 + options: { 25 + isPublic?: boolean // TODO(pwi) need to enable in TF somehow 26 + } = {}, 19 27 ): React.FC<P> => 20 28 function AuthRequired(props: P) { 21 29 const {isInitialLoad, hasSession} = useSession() 22 30 const onboardingState = useOnboardingState() 31 + const {showLoggedOut} = useLoggedOutView() 32 + const {setShowLoggedOut} = useLoggedOutViewControls() 33 + 23 34 if (isInitialLoad) { 24 35 return <Loading /> 25 36 } 26 37 if (!hasSession) { 27 - return <LoggedOut /> 38 + if (showLoggedOut) { 39 + return <LoggedOut onDismiss={() => setShowLoggedOut(false)} /> 40 + } else if (!options?.isPublic || IS_PROD) { 41 + return <LoggedOut /> 42 + } 28 43 } 29 44 if (onboardingState.isActive) { 30 45 return <Onboarding />
+8 -2
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 25 25 } from '#/state/queries/post' 26 26 import {useComposerControls} from '#/state/shell/composer' 27 27 import {Shadow} from '#/state/cache/types' 28 + import {useRequireAuth} from '#/state/session' 28 29 29 30 export function PostCtrls({ 30 31 big, ··· 46 47 const postUnlikeMutation = usePostUnlikeMutation() 47 48 const postRepostMutation = usePostRepostMutation() 48 49 const postUnrepostMutation = usePostUnrepostMutation() 50 + const requireAuth = useRequireAuth() 49 51 50 52 const defaultCtrlColor = React.useMemo( 51 53 () => ({ ··· 107 109 <TouchableOpacity 108 110 testID="replyBtn" 109 111 style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]} 110 - onPress={onPressReply} 112 + onPress={() => { 113 + requireAuth(() => onPressReply()) 114 + }} 111 115 accessibilityRole="button" 112 116 accessibilityLabel={`Reply (${post.replyCount} ${ 113 117 post.replyCount === 1 ? 'reply' : 'replies' ··· 135 139 <TouchableOpacity 136 140 testID="likeBtn" 137 141 style={[styles.ctrl, !big && styles.ctrlPad]} 138 - onPress={onPressToggleLike} 142 + onPress={() => { 143 + requireAuth(() => onPressToggleLike()) 144 + }} 139 145 accessibilityRole="button" 140 146 accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${ 141 147 post.likeCount
+5 -1
src/view/com/util/post-ctrls/RepostButton.tsx
··· 7 7 import {pluralize} from 'lib/strings/helpers' 8 8 import {HITSLOP_10, HITSLOP_20} from 'lib/constants' 9 9 import {useModalControls} from '#/state/modals' 10 + import {useRequireAuth} from '#/state/session' 10 11 11 12 interface Props { 12 13 isReposted: boolean ··· 25 26 }: Props) => { 26 27 const theme = useTheme() 27 28 const {openModal} = useModalControls() 29 + const requireAuth = useRequireAuth() 28 30 29 31 const defaultControlColor = React.useMemo( 30 32 () => ({ ··· 45 47 return ( 46 48 <TouchableOpacity 47 49 testID="repostBtn" 48 - onPress={onPressToggleRepostWrapper} 50 + onPress={() => { 51 + requireAuth(() => onPressToggleRepostWrapper()) 52 + }} 49 53 style={[styles.control, !big && styles.controlPad]} 50 54 accessibilityRole="button" 51 55 accessibilityLabel={`${
+38 -20
src/view/com/util/post-ctrls/RepostButton.web.tsx
··· 1 1 import React from 'react' 2 - import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' 2 + import {StyleProp, StyleSheet, View, ViewStyle, Pressable} from 'react-native' 3 3 import {RepostIcon} from 'lib/icons' 4 4 import {colors} from 'lib/styles' 5 5 import {useTheme} from 'lib/ThemeContext' ··· 12 12 import {EventStopper} from '../EventStopper' 13 13 import {useLingui} from '@lingui/react' 14 14 import {msg} from '@lingui/macro' 15 + import {useRequireAuth} from '#/state/session' 16 + import {useSession} from '#/state/session' 15 17 16 18 interface Props { 17 19 isReposted: boolean ··· 31 33 }: Props) => { 32 34 const theme = useTheme() 33 35 const {_} = useLingui() 36 + const {hasSession} = useSession() 37 + const requireAuth = useRequireAuth() 34 38 35 39 const defaultControlColor = React.useMemo( 36 40 () => ({ ··· 62 66 }, 63 67 ] 64 68 65 - return ( 69 + const inner = ( 70 + <View 71 + style={[ 72 + styles.control, 73 + !big && styles.controlPad, 74 + (isReposted 75 + ? styles.reposted 76 + : defaultControlColor) as StyleProp<ViewStyle>, 77 + ]}> 78 + <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} /> 79 + {typeof repostCount !== 'undefined' ? ( 80 + <Text 81 + testID="repostCount" 82 + type={isReposted ? 'md-bold' : 'md'} 83 + style={styles.repostCount}> 84 + {repostCount ?? 0} 85 + </Text> 86 + ) : undefined} 87 + </View> 88 + ) 89 + 90 + return hasSession ? ( 66 91 <EventStopper> 67 92 <NativeDropdown 68 93 items={dropdownItems} 69 94 accessibilityLabel={_(msg`Repost or quote post`)} 70 95 accessibilityHint=""> 71 - <View 72 - style={[ 73 - styles.control, 74 - !big && styles.controlPad, 75 - (isReposted 76 - ? styles.reposted 77 - : defaultControlColor) as StyleProp<ViewStyle>, 78 - ]}> 79 - <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} /> 80 - {typeof repostCount !== 'undefined' ? ( 81 - <Text 82 - testID="repostCount" 83 - type={isReposted ? 'md-bold' : 'md'} 84 - style={styles.repostCount}> 85 - {repostCount ?? 0} 86 - </Text> 87 - ) : undefined} 88 - </View> 96 + {inner} 89 97 </NativeDropdown> 90 98 </EventStopper> 99 + ) : ( 100 + <Pressable 101 + accessibilityRole="button" 102 + onPress={() => { 103 + requireAuth(() => {}) 104 + }} 105 + accessibilityLabel={_(msg`Repost or quote post`)} 106 + accessibilityHint=""> 107 + {inner} 108 + </Pressable> 91 109 ) 92 110 } 93 111
+370 -367
src/view/screens/Feeds.tsx
··· 87 87 key: string 88 88 } 89 89 90 - export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( 91 - _props: Props, 92 - ) { 93 - const pal = usePalette('default') 94 - const {openComposer} = useComposerControls() 95 - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() 96 - const [query, setQuery] = React.useState('') 97 - const [isPTR, setIsPTR] = React.useState(false) 98 - const { 99 - data: preferences, 100 - isLoading: isPreferencesLoading, 101 - error: preferencesError, 102 - } = usePreferencesQuery() 103 - const { 104 - data: popularFeeds, 105 - isFetching: isPopularFeedsFetching, 106 - error: popularFeedsError, 107 - refetch: refetchPopularFeeds, 108 - fetchNextPage: fetchNextPopularFeedsPage, 109 - isFetchingNextPage: isPopularFeedsFetchingNextPage, 110 - hasNextPage: hasNextPopularFeedsPage, 111 - } = useGetPopularFeedsQuery() 112 - const {_} = useLingui() 113 - const setMinimalShellMode = useSetMinimalShellMode() 114 - const { 115 - data: searchResults, 116 - mutate: search, 117 - reset: resetSearch, 118 - isPending: isSearchPending, 119 - error: searchError, 120 - } = useSearchPopularFeedsMutation() 90 + export const FeedsScreen = withAuthRequired( 91 + function FeedsScreenImpl(_props: Props) { 92 + const pal = usePalette('default') 93 + const {openComposer} = useComposerControls() 94 + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() 95 + const [query, setQuery] = React.useState('') 96 + const [isPTR, setIsPTR] = React.useState(false) 97 + const { 98 + data: preferences, 99 + isLoading: isPreferencesLoading, 100 + error: preferencesError, 101 + } = usePreferencesQuery() 102 + const { 103 + data: popularFeeds, 104 + isFetching: isPopularFeedsFetching, 105 + error: popularFeedsError, 106 + refetch: refetchPopularFeeds, 107 + fetchNextPage: fetchNextPopularFeedsPage, 108 + isFetchingNextPage: isPopularFeedsFetchingNextPage, 109 + hasNextPage: hasNextPopularFeedsPage, 110 + } = useGetPopularFeedsQuery() 111 + const {_} = useLingui() 112 + const setMinimalShellMode = useSetMinimalShellMode() 113 + const { 114 + data: searchResults, 115 + mutate: search, 116 + reset: resetSearch, 117 + isPending: isSearchPending, 118 + error: searchError, 119 + } = useSearchPopularFeedsMutation() 121 120 122 - /** 123 - * A search query is present. We may not have search results yet. 124 - */ 125 - const isUserSearching = query.length > 1 126 - const debouncedSearch = React.useMemo( 127 - () => debounce(q => search(q), 500), // debounce for 500ms 128 - [search], 129 - ) 130 - const onPressCompose = React.useCallback(() => { 131 - openComposer({}) 132 - }, [openComposer]) 133 - const onChangeQuery = React.useCallback( 134 - (text: string) => { 135 - setQuery(text) 136 - if (text.length > 1) { 137 - debouncedSearch(text) 138 - } else { 139 - refetchPopularFeeds() 140 - resetSearch() 141 - } 142 - }, 143 - [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch], 144 - ) 145 - const onPressCancelSearch = React.useCallback(() => { 146 - setQuery('') 147 - refetchPopularFeeds() 148 - resetSearch() 149 - }, [refetchPopularFeeds, setQuery, resetSearch]) 150 - const onSubmitQuery = React.useCallback(() => { 151 - debouncedSearch(query) 152 - }, [query, debouncedSearch]) 153 - const onPullToRefresh = React.useCallback(async () => { 154 - setIsPTR(true) 155 - await refetchPopularFeeds() 156 - setIsPTR(false) 157 - }, [setIsPTR, refetchPopularFeeds]) 158 - const onEndReached = React.useCallback(() => { 159 - if ( 160 - isPopularFeedsFetching || 161 - isUserSearching || 162 - !hasNextPopularFeedsPage || 163 - popularFeedsError 121 + /** 122 + * A search query is present. We may not have search results yet. 123 + */ 124 + const isUserSearching = query.length > 1 125 + const debouncedSearch = React.useMemo( 126 + () => debounce(q => search(q), 500), // debounce for 500ms 127 + [search], 128 + ) 129 + const onPressCompose = React.useCallback(() => { 130 + openComposer({}) 131 + }, [openComposer]) 132 + const onChangeQuery = React.useCallback( 133 + (text: string) => { 134 + setQuery(text) 135 + if (text.length > 1) { 136 + debouncedSearch(text) 137 + } else { 138 + refetchPopularFeeds() 139 + resetSearch() 140 + } 141 + }, 142 + [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch], 164 143 ) 165 - return 166 - fetchNextPopularFeedsPage() 167 - }, [ 168 - isPopularFeedsFetching, 169 - isUserSearching, 170 - popularFeedsError, 171 - hasNextPopularFeedsPage, 172 - fetchNextPopularFeedsPage, 173 - ]) 174 - 175 - useFocusEffect( 176 - React.useCallback(() => { 177 - setMinimalShellMode(false) 178 - }, [setMinimalShellMode]), 179 - ) 144 + const onPressCancelSearch = React.useCallback(() => { 145 + setQuery('') 146 + refetchPopularFeeds() 147 + resetSearch() 148 + }, [refetchPopularFeeds, setQuery, resetSearch]) 149 + const onSubmitQuery = React.useCallback(() => { 150 + debouncedSearch(query) 151 + }, [query, debouncedSearch]) 152 + const onPullToRefresh = React.useCallback(async () => { 153 + setIsPTR(true) 154 + await refetchPopularFeeds() 155 + setIsPTR(false) 156 + }, [setIsPTR, refetchPopularFeeds]) 157 + const onEndReached = React.useCallback(() => { 158 + if ( 159 + isPopularFeedsFetching || 160 + isUserSearching || 161 + !hasNextPopularFeedsPage || 162 + popularFeedsError 163 + ) 164 + return 165 + fetchNextPopularFeedsPage() 166 + }, [ 167 + isPopularFeedsFetching, 168 + isUserSearching, 169 + popularFeedsError, 170 + hasNextPopularFeedsPage, 171 + fetchNextPopularFeedsPage, 172 + ]) 180 173 181 - const items = React.useMemo(() => { 182 - let slices: FlatlistSlice[] = [] 174 + useFocusEffect( 175 + React.useCallback(() => { 176 + setMinimalShellMode(false) 177 + }, [setMinimalShellMode]), 178 + ) 183 179 184 - slices.push({ 185 - key: 'savedFeedsHeader', 186 - type: 'savedFeedsHeader', 187 - }) 180 + const items = React.useMemo(() => { 181 + let slices: FlatlistSlice[] = [] 188 182 189 - if (preferencesError) { 190 183 slices.push({ 191 - key: 'savedFeedsError', 192 - type: 'error', 193 - error: cleanError(preferencesError.toString()), 184 + key: 'savedFeedsHeader', 185 + type: 'savedFeedsHeader', 194 186 }) 195 - } else { 196 - if (isPreferencesLoading || !preferences?.feeds?.saved) { 187 + 188 + if (preferencesError) { 197 189 slices.push({ 198 - key: 'savedFeedsLoading', 199 - type: 'savedFeedsLoading', 200 - // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, 190 + key: 'savedFeedsError', 191 + type: 'error', 192 + error: cleanError(preferencesError.toString()), 201 193 }) 202 194 } else { 203 - if (preferences?.feeds?.saved.length === 0) { 195 + if (isPreferencesLoading || !preferences?.feeds?.saved) { 204 196 slices.push({ 205 - key: 'savedFeedNoResults', 206 - type: 'savedFeedNoResults', 197 + key: 'savedFeedsLoading', 198 + type: 'savedFeedsLoading', 199 + // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, 207 200 }) 208 201 } else { 209 - const {saved, pinned} = preferences.feeds 210 - 211 - slices = slices.concat( 212 - pinned.map(uri => ({ 213 - key: `savedFeed:${uri}`, 214 - type: 'savedFeed', 215 - feedUri: uri, 216 - })), 217 - ) 202 + if (preferences?.feeds?.saved.length === 0) { 203 + slices.push({ 204 + key: 'savedFeedNoResults', 205 + type: 'savedFeedNoResults', 206 + }) 207 + } else { 208 + const {saved, pinned} = preferences.feeds 218 209 219 - slices = slices.concat( 220 - saved 221 - .filter(uri => !pinned.includes(uri)) 222 - .map(uri => ({ 210 + slices = slices.concat( 211 + pinned.map(uri => ({ 223 212 key: `savedFeed:${uri}`, 224 213 type: 'savedFeed', 225 214 feedUri: uri, 226 215 })), 227 - ) 216 + ) 217 + 218 + slices = slices.concat( 219 + saved 220 + .filter(uri => !pinned.includes(uri)) 221 + .map(uri => ({ 222 + key: `savedFeed:${uri}`, 223 + type: 'savedFeed', 224 + feedUri: uri, 225 + })), 226 + ) 227 + } 228 228 } 229 229 } 230 - } 231 230 232 - slices.push({ 233 - key: 'popularFeedsHeader', 234 - type: 'popularFeedsHeader', 235 - }) 236 - 237 - if (popularFeedsError || searchError) { 238 231 slices.push({ 239 - key: 'popularFeedsError', 240 - type: 'error', 241 - error: cleanError( 242 - popularFeedsError?.toString() ?? searchError?.toString() ?? '', 243 - ), 232 + key: 'popularFeedsHeader', 233 + type: 'popularFeedsHeader', 244 234 }) 245 - } else { 246 - if (isUserSearching) { 247 - if (isSearchPending || !searchResults) { 248 - slices.push({ 249 - key: 'popularFeedsLoading', 250 - type: 'popularFeedsLoading', 251 - }) 252 - } else { 253 - if (!searchResults || searchResults?.length === 0) { 235 + 236 + if (popularFeedsError || searchError) { 237 + slices.push({ 238 + key: 'popularFeedsError', 239 + type: 'error', 240 + error: cleanError( 241 + popularFeedsError?.toString() ?? searchError?.toString() ?? '', 242 + ), 243 + }) 244 + } else { 245 + if (isUserSearching) { 246 + if (isSearchPending || !searchResults) { 254 247 slices.push({ 255 - key: 'popularFeedsNoResults', 256 - type: 'popularFeedsNoResults', 248 + key: 'popularFeedsLoading', 249 + type: 'popularFeedsLoading', 257 250 }) 258 251 } else { 259 - slices = slices.concat( 260 - searchResults.map(feed => ({ 261 - key: `popularFeed:${feed.uri}`, 262 - type: 'popularFeed', 263 - feedUri: feed.uri, 264 - })), 265 - ) 252 + if (!searchResults || searchResults?.length === 0) { 253 + slices.push({ 254 + key: 'popularFeedsNoResults', 255 + type: 'popularFeedsNoResults', 256 + }) 257 + } else { 258 + slices = slices.concat( 259 + searchResults.map(feed => ({ 260 + key: `popularFeed:${feed.uri}`, 261 + type: 'popularFeed', 262 + feedUri: feed.uri, 263 + })), 264 + ) 265 + } 266 266 } 267 - } 268 - } else { 269 - if (isPopularFeedsFetching && !popularFeeds?.pages) { 270 - slices.push({ 271 - key: 'popularFeedsLoading', 272 - type: 'popularFeedsLoading', 273 - }) 274 267 } else { 275 - if ( 276 - !popularFeeds?.pages || 277 - popularFeeds?.pages[0]?.feeds?.length === 0 278 - ) { 268 + if (isPopularFeedsFetching && !popularFeeds?.pages) { 279 269 slices.push({ 280 - key: 'popularFeedsNoResults', 281 - type: 'popularFeedsNoResults', 270 + key: 'popularFeedsLoading', 271 + type: 'popularFeedsLoading', 282 272 }) 283 273 } else { 284 - for (const page of popularFeeds.pages || []) { 285 - slices = slices.concat( 286 - page.feeds 287 - .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) 288 - .map(feed => ({ 289 - key: `popularFeed:${feed.uri}`, 290 - type: 'popularFeed', 291 - feedUri: feed.uri, 292 - })), 293 - ) 294 - } 295 - 296 - if (isPopularFeedsFetchingNextPage) { 274 + if ( 275 + !popularFeeds?.pages || 276 + popularFeeds?.pages[0]?.feeds?.length === 0 277 + ) { 297 278 slices.push({ 298 - key: 'popularFeedsLoadingMore', 299 - type: 'popularFeedsLoadingMore', 279 + key: 'popularFeedsNoResults', 280 + type: 'popularFeedsNoResults', 300 281 }) 282 + } else { 283 + for (const page of popularFeeds.pages || []) { 284 + slices = slices.concat( 285 + page.feeds 286 + .filter( 287 + feed => !preferences?.feeds?.saved.includes(feed.uri), 288 + ) 289 + .map(feed => ({ 290 + key: `popularFeed:${feed.uri}`, 291 + type: 'popularFeed', 292 + feedUri: feed.uri, 293 + })), 294 + ) 295 + } 296 + 297 + if (isPopularFeedsFetchingNextPage) { 298 + slices.push({ 299 + key: 'popularFeedsLoadingMore', 300 + type: 'popularFeedsLoadingMore', 301 + }) 302 + } 301 303 } 302 304 } 303 305 } 304 306 } 305 - } 306 307 307 - return slices 308 - }, [ 309 - preferences, 310 - isPreferencesLoading, 311 - preferencesError, 312 - popularFeeds, 313 - isPopularFeedsFetching, 314 - popularFeedsError, 315 - isPopularFeedsFetchingNextPage, 316 - searchResults, 317 - isSearchPending, 318 - searchError, 319 - isUserSearching, 320 - ]) 308 + return slices 309 + }, [ 310 + preferences, 311 + isPreferencesLoading, 312 + preferencesError, 313 + popularFeeds, 314 + isPopularFeedsFetching, 315 + popularFeedsError, 316 + isPopularFeedsFetchingNextPage, 317 + searchResults, 318 + isSearchPending, 319 + searchError, 320 + isUserSearching, 321 + ]) 321 322 322 - const renderHeaderBtn = React.useCallback(() => { 323 - return ( 324 - <Link 325 - href="/settings/saved-feeds" 326 - hitSlop={10} 327 - accessibilityRole="button" 328 - accessibilityLabel={_(msg`Edit Saved Feeds`)} 329 - accessibilityHint="Opens screen to edit Saved Feeds"> 330 - <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> 331 - </Link> 332 - ) 333 - }, [pal, _]) 323 + const renderHeaderBtn = React.useCallback(() => { 324 + return ( 325 + <Link 326 + href="/settings/saved-feeds" 327 + hitSlop={10} 328 + accessibilityRole="button" 329 + accessibilityLabel={_(msg`Edit Saved Feeds`)} 330 + accessibilityHint="Opens screen to edit Saved Feeds"> 331 + <CogIcon size={22} strokeWidth={2} style={pal.textLight} /> 332 + </Link> 333 + ) 334 + }, [pal, _]) 334 335 335 - const renderItem = React.useCallback( 336 - ({item}: {item: FlatlistSlice}) => { 337 - if (item.type === 'error') { 338 - return <ErrorMessage message={item.error} /> 339 - } else if ( 340 - item.type === 'popularFeedsLoadingMore' || 341 - item.type === 'savedFeedsLoading' 342 - ) { 343 - return ( 344 - <View style={s.p10}> 345 - <ActivityIndicator /> 346 - </View> 347 - ) 348 - } else if (item.type === 'savedFeedsHeader') { 349 - if (!isMobile) { 336 + const renderItem = React.useCallback( 337 + ({item}: {item: FlatlistSlice}) => { 338 + if (item.type === 'error') { 339 + return <ErrorMessage message={item.error} /> 340 + } else if ( 341 + item.type === 'popularFeedsLoadingMore' || 342 + item.type === 'savedFeedsLoading' 343 + ) { 350 344 return ( 351 - <View 352 - style={[ 353 - pal.view, 354 - styles.header, 355 - pal.border, 356 - { 357 - borderBottomWidth: 1, 358 - }, 359 - ]}> 360 - <Text type="title-lg" style={[pal.text, s.bold]}> 361 - <Trans>My Feeds</Trans> 362 - </Text> 363 - <Link 364 - href="/settings/saved-feeds" 365 - accessibilityLabel={_(msg`Edit My Feeds`)} 366 - accessibilityHint=""> 367 - <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> 368 - </Link> 345 + <View style={s.p10}> 346 + <ActivityIndicator /> 369 347 </View> 370 348 ) 371 - } 372 - return <View /> 373 - } else if (item.type === 'savedFeedNoResults') { 374 - return ( 375 - <View 376 - style={{ 377 - paddingHorizontal: 16, 378 - paddingTop: 10, 379 - }}> 380 - <Text type="lg" style={pal.textLight}> 381 - <Trans>You don't have any saved feeds!</Trans> 382 - </Text> 383 - </View> 384 - ) 385 - } else if (item.type === 'savedFeed') { 386 - return <SavedFeed feedUri={item.feedUri} /> 387 - } else if (item.type === 'popularFeedsHeader') { 388 - return ( 389 - <> 349 + } else if (item.type === 'savedFeedsHeader') { 350 + if (!isMobile) { 351 + return ( 352 + <View 353 + style={[ 354 + pal.view, 355 + styles.header, 356 + pal.border, 357 + { 358 + borderBottomWidth: 1, 359 + }, 360 + ]}> 361 + <Text type="title-lg" style={[pal.text, s.bold]}> 362 + <Trans>My Feeds</Trans> 363 + </Text> 364 + <Link 365 + href="/settings/saved-feeds" 366 + accessibilityLabel={_(msg`Edit My Feeds`)} 367 + accessibilityHint=""> 368 + <CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> 369 + </Link> 370 + </View> 371 + ) 372 + } 373 + return <View /> 374 + } else if (item.type === 'savedFeedNoResults') { 375 + return ( 390 376 <View 391 - style={[ 392 - pal.view, 393 - styles.header, 394 - { 395 - marginTop: 16, 396 - paddingLeft: isMobile ? 12 : undefined, 397 - paddingRight: 10, 398 - paddingBottom: isMobile ? 6 : undefined, 399 - }, 400 - ]}> 401 - <Text type="title-lg" style={[pal.text, s.bold]}> 402 - <Trans>Discover new feeds</Trans> 377 + style={{ 378 + paddingHorizontal: 16, 379 + paddingTop: 10, 380 + }}> 381 + <Text type="lg" style={pal.textLight}> 382 + <Trans>You don't have any saved feeds!</Trans> 403 383 </Text> 384 + </View> 385 + ) 386 + } else if (item.type === 'savedFeed') { 387 + return <SavedFeed feedUri={item.feedUri} /> 388 + } else if (item.type === 'popularFeedsHeader') { 389 + return ( 390 + <> 391 + <View 392 + style={[ 393 + pal.view, 394 + styles.header, 395 + { 396 + marginTop: 16, 397 + paddingLeft: isMobile ? 12 : undefined, 398 + paddingRight: 10, 399 + paddingBottom: isMobile ? 6 : undefined, 400 + }, 401 + ]}> 402 + <Text type="title-lg" style={[pal.text, s.bold]}> 403 + <Trans>Discover new feeds</Trans> 404 + </Text> 404 405 405 - {!isMobile && ( 406 - <SearchInput 407 - query={query} 408 - onChangeQuery={onChangeQuery} 409 - onPressCancelSearch={onPressCancelSearch} 410 - onSubmitQuery={onSubmitQuery} 411 - style={{flex: 1, maxWidth: 250}} 412 - /> 406 + {!isMobile && ( 407 + <SearchInput 408 + query={query} 409 + onChangeQuery={onChangeQuery} 410 + onPressCancelSearch={onPressCancelSearch} 411 + onSubmitQuery={onSubmitQuery} 412 + style={{flex: 1, maxWidth: 250}} 413 + /> 414 + )} 415 + </View> 416 + 417 + {isMobile && ( 418 + <View style={{paddingHorizontal: 8, paddingBottom: 10}}> 419 + <SearchInput 420 + query={query} 421 + onChangeQuery={onChangeQuery} 422 + onPressCancelSearch={onPressCancelSearch} 423 + onSubmitQuery={onSubmitQuery} 424 + /> 425 + </View> 413 426 )} 427 + </> 428 + ) 429 + } else if (item.type === 'popularFeedsLoading') { 430 + return <FeedFeedLoadingPlaceholder /> 431 + } else if (item.type === 'popularFeed') { 432 + return ( 433 + <FeedSourceCard 434 + feedUri={item.feedUri} 435 + showSaveBtn 436 + showDescription 437 + showLikes 438 + /> 439 + ) 440 + } else if (item.type === 'popularFeedsNoResults') { 441 + return ( 442 + <View 443 + style={{ 444 + paddingHorizontal: 16, 445 + paddingTop: 10, 446 + paddingBottom: '150%', 447 + }}> 448 + <Text type="lg" style={pal.textLight}> 449 + <Trans>No results found for "{query}"</Trans> 450 + </Text> 414 451 </View> 452 + ) 453 + } 454 + return null 455 + }, 456 + [ 457 + _, 458 + isMobile, 459 + pal, 460 + query, 461 + onChangeQuery, 462 + onPressCancelSearch, 463 + onSubmitQuery, 464 + ], 465 + ) 415 466 416 - {isMobile && ( 417 - <View style={{paddingHorizontal: 8, paddingBottom: 10}}> 418 - <SearchInput 419 - query={query} 420 - onChangeQuery={onChangeQuery} 421 - onPressCancelSearch={onPressCancelSearch} 422 - onSubmitQuery={onSubmitQuery} 423 - /> 424 - </View> 425 - )} 426 - </> 427 - ) 428 - } else if (item.type === 'popularFeedsLoading') { 429 - return <FeedFeedLoadingPlaceholder /> 430 - } else if (item.type === 'popularFeed') { 431 - return ( 432 - <FeedSourceCard 433 - feedUri={item.feedUri} 434 - showSaveBtn 435 - showDescription 436 - showLikes 467 + return ( 468 + <View style={[pal.view, styles.container]}> 469 + {isMobile && ( 470 + <ViewHeader 471 + title={_(msg`Feeds`)} 472 + canGoBack={false} 473 + renderButton={renderHeaderBtn} 474 + showBorder 437 475 /> 438 - ) 439 - } else if (item.type === 'popularFeedsNoResults') { 440 - return ( 441 - <View 442 - style={{ 443 - paddingHorizontal: 16, 444 - paddingTop: 10, 445 - paddingBottom: '150%', 446 - }}> 447 - <Text type="lg" style={pal.textLight}> 448 - <Trans>No results found for "{query}"</Trans> 449 - </Text> 450 - </View> 451 - ) 452 - } 453 - return null 454 - }, 455 - [ 456 - _, 457 - isMobile, 458 - pal, 459 - query, 460 - onChangeQuery, 461 - onPressCancelSearch, 462 - onSubmitQuery, 463 - ], 464 - ) 465 - 466 - return ( 467 - <View style={[pal.view, styles.container]}> 468 - {isMobile && ( 469 - <ViewHeader 470 - title={_(msg`Feeds`)} 471 - canGoBack={false} 472 - renderButton={renderHeaderBtn} 473 - showBorder 474 - /> 475 - )} 476 + )} 476 477 477 - {preferences ? <View /> : <ActivityIndicator />} 478 + {preferences ? <View /> : <ActivityIndicator />} 478 479 479 - <FlatList 480 - style={[!isTabletOrDesktop && s.flex1, styles.list]} 481 - data={items} 482 - keyExtractor={item => item.key} 483 - contentContainerStyle={styles.contentContainer} 484 - renderItem={renderItem} 485 - refreshControl={ 486 - <RefreshControl 487 - refreshing={isPTR} 488 - onRefresh={isUserSearching ? undefined : onPullToRefresh} 489 - tintColor={pal.colors.text} 490 - titleColor={pal.colors.text} 491 - /> 492 - } 493 - initialNumToRender={10} 494 - onEndReached={onEndReached} 495 - // @ts-ignore our .web version only -prf 496 - desktopFixedHeight 497 - /> 480 + <FlatList 481 + style={[!isTabletOrDesktop && s.flex1, styles.list]} 482 + data={items} 483 + keyExtractor={item => item.key} 484 + contentContainerStyle={styles.contentContainer} 485 + renderItem={renderItem} 486 + refreshControl={ 487 + <RefreshControl 488 + refreshing={isPTR} 489 + onRefresh={isUserSearching ? undefined : onPullToRefresh} 490 + tintColor={pal.colors.text} 491 + titleColor={pal.colors.text} 492 + /> 493 + } 494 + initialNumToRender={10} 495 + onEndReached={onEndReached} 496 + // @ts-ignore our .web version only -prf 497 + desktopFixedHeight 498 + /> 498 499 499 - <FAB 500 - testID="composeFAB" 501 - onPress={onPressCompose} 502 - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 503 - accessibilityRole="button" 504 - accessibilityLabel={_(msg`New post`)} 505 - accessibilityHint="" 506 - /> 507 - </View> 508 - ) 509 - }) 500 + <FAB 501 + testID="composeFAB" 502 + onPress={onPressCompose} 503 + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} 504 + accessibilityRole="button" 505 + accessibilityLabel={_(msg`New post`)} 506 + accessibilityHint="" 507 + /> 508 + </View> 509 + ) 510 + }, 511 + {isPublic: true}, 512 + ) 510 513 511 514 function SavedFeed({feedUri}: {feedUri: string}) { 512 515 const pal = usePalette('default')
+49 -14
src/view/screens/Home.tsx
··· 14 14 import {usePreferencesQuery} from '#/state/queries/preferences' 15 15 import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types' 16 16 import {emitSoftReset} from '#/state/events' 17 + import {useSession} from '#/state/session' 17 18 18 19 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> 19 - export const HomeScreen = withAuthRequired(function HomeScreenImpl( 20 - props: Props, 21 - ) { 22 - const {data: preferences} = usePreferencesQuery() 23 - if (preferences) { 24 - return <HomeScreenReady {...props} preferences={preferences} /> 25 - } else { 26 - return ( 27 - <View style={styles.loading}> 28 - <ActivityIndicator size="large" /> 29 - </View> 30 - ) 31 - } 32 - }) 20 + export const HomeScreen = withAuthRequired( 21 + function HomeScreenImpl(props: Props) { 22 + const {hasSession} = useSession() 23 + const {data: preferences} = usePreferencesQuery() 24 + 25 + if (!hasSession) { 26 + return <HomeScreenPublic /> 27 + } 28 + 29 + if (preferences) { 30 + return <HomeScreenReady {...props} preferences={preferences} /> 31 + } else { 32 + return ( 33 + <View style={styles.loading}> 34 + <ActivityIndicator size="large" /> 35 + </View> 36 + ) 37 + } 38 + }, 39 + { 40 + isPublic: true, 41 + }, 42 + ) 43 + 44 + function HomeScreenPublic() { 45 + const setMinimalShellMode = useSetMinimalShellMode() 46 + const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() 47 + 48 + const renderCustomFeedEmptyState = React.useCallback(() => { 49 + return <CustomFeedEmptyState /> 50 + }, []) 51 + 52 + useFocusEffect( 53 + React.useCallback(() => { 54 + setMinimalShellMode(false) 55 + setDrawerSwipeDisabled(false) 56 + }, [setDrawerSwipeDisabled, setMinimalShellMode]), 57 + ) 58 + 59 + return ( 60 + <FeedPage 61 + isPageFocused 62 + feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`} 63 + renderEmptyState={renderCustomFeedEmptyState} 64 + /> 65 + ) 66 + } 33 67 34 68 function HomeScreenReady({ 35 69 preferences, ··· 83 117 emitSoftReset() 84 118 }, []) 85 119 120 + // TODO(pwi) may need this in public view 86 121 const onPageScrollStateChanged = React.useCallback( 87 122 (state: 'idle' | 'dragging' | 'settling') => { 88 123 if (state === 'dragging') {
+20 -17
src/view/screens/PostLikedBy.tsx
··· 11 11 import {useLingui} from '@lingui/react' 12 12 13 13 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> 14 - export const PostLikedByScreen = withAuthRequired(({route}: Props) => { 15 - const setMinimalShellMode = useSetMinimalShellMode() 16 - const {name, rkey} = route.params 17 - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 18 - const {_} = useLingui() 14 + export const PostLikedByScreen = withAuthRequired( 15 + ({route}: Props) => { 16 + const setMinimalShellMode = useSetMinimalShellMode() 17 + const {name, rkey} = route.params 18 + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 19 + const {_} = useLingui() 19 20 20 - useFocusEffect( 21 - React.useCallback(() => { 22 - setMinimalShellMode(false) 23 - }, [setMinimalShellMode]), 24 - ) 21 + useFocusEffect( 22 + React.useCallback(() => { 23 + setMinimalShellMode(false) 24 + }, [setMinimalShellMode]), 25 + ) 25 26 26 - return ( 27 - <View> 28 - <ViewHeader title={_(msg`Liked by`)} /> 29 - <PostLikedByComponent uri={uri} /> 30 - </View> 31 - ) 32 - }) 27 + return ( 28 + <View> 29 + <ViewHeader title={_(msg`Liked by`)} /> 30 + <PostLikedByComponent uri={uri} /> 31 + </View> 32 + ) 33 + }, 34 + {isPublic: true}, 35 + )
+20 -17
src/view/screens/PostRepostedBy.tsx
··· 11 11 import {msg} from '@lingui/macro' 12 12 13 13 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'> 14 - export const PostRepostedByScreen = withAuthRequired(({route}: Props) => { 15 - const {name, rkey} = route.params 16 - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 17 - const setMinimalShellMode = useSetMinimalShellMode() 18 - const {_} = useLingui() 14 + export const PostRepostedByScreen = withAuthRequired( 15 + ({route}: Props) => { 16 + const {name, rkey} = route.params 17 + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 18 + const setMinimalShellMode = useSetMinimalShellMode() 19 + const {_} = useLingui() 19 20 20 - useFocusEffect( 21 - React.useCallback(() => { 22 - setMinimalShellMode(false) 23 - }, [setMinimalShellMode]), 24 - ) 21 + useFocusEffect( 22 + React.useCallback(() => { 23 + setMinimalShellMode(false) 24 + }, [setMinimalShellMode]), 25 + ) 25 26 26 - return ( 27 - <View> 28 - <ViewHeader title={_(msg`Reposted by`)} /> 29 - <PostRepostedByComponent uri={uri} /> 30 - </View> 31 - ) 32 - }) 27 + return ( 28 + <View> 29 + <ViewHeader title={_(msg`Reposted by`)} /> 30 + <PostRepostedByComponent uri={uri} /> 31 + </View> 32 + ) 33 + }, 34 + {isPublic: true}, 35 + )
+73 -72
src/view/screens/PostThread.tsx
··· 27 27 import {useComposerControls} from '#/state/shell/composer' 28 28 29 29 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'> 30 - export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({ 31 - route, 32 - }: Props) { 33 - const queryClient = useQueryClient() 34 - const {_} = useLingui() 35 - const {fabMinimalShellTransform} = useMinimalShellMode() 36 - const setMinimalShellMode = useSetMinimalShellMode() 37 - const {openComposer} = useComposerControls() 38 - const safeAreaInsets = useSafeAreaInsets() 39 - const {name, rkey} = route.params 40 - const {isMobile} = useWebMediaQueries() 41 - const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 42 - const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) 30 + export const PostThreadScreen = withAuthRequired( 31 + function PostThreadScreenImpl({route}: Props) { 32 + const queryClient = useQueryClient() 33 + const {_} = useLingui() 34 + const {fabMinimalShellTransform} = useMinimalShellMode() 35 + const setMinimalShellMode = useSetMinimalShellMode() 36 + const {openComposer} = useComposerControls() 37 + const safeAreaInsets = useSafeAreaInsets() 38 + const {name, rkey} = route.params 39 + const {isMobile} = useWebMediaQueries() 40 + const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) 41 + const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri) 43 42 44 - useFocusEffect( 45 - React.useCallback(() => { 46 - setMinimalShellMode(false) 47 - }, [setMinimalShellMode]), 48 - ) 43 + useFocusEffect( 44 + React.useCallback(() => { 45 + setMinimalShellMode(false) 46 + }, [setMinimalShellMode]), 47 + ) 49 48 50 - const onPressReply = React.useCallback(() => { 51 - if (!resolvedUri) { 52 - return 53 - } 54 - const thread = queryClient.getQueryData<ThreadNode>( 55 - POST_THREAD_RQKEY(resolvedUri.uri), 56 - ) 57 - if (thread?.type !== 'post') { 58 - return 59 - } 60 - openComposer({ 61 - replyTo: { 62 - uri: thread.post.uri, 63 - cid: thread.post.cid, 64 - text: thread.record.text, 65 - author: { 66 - handle: thread.post.author.handle, 67 - displayName: thread.post.author.displayName, 68 - avatar: thread.post.author.avatar, 49 + const onPressReply = React.useCallback(() => { 50 + if (!resolvedUri) { 51 + return 52 + } 53 + const thread = queryClient.getQueryData<ThreadNode>( 54 + POST_THREAD_RQKEY(resolvedUri.uri), 55 + ) 56 + if (thread?.type !== 'post') { 57 + return 58 + } 59 + openComposer({ 60 + replyTo: { 61 + uri: thread.post.uri, 62 + cid: thread.post.cid, 63 + text: thread.record.text, 64 + author: { 65 + handle: thread.post.author.handle, 66 + displayName: thread.post.author.displayName, 67 + avatar: thread.post.author.avatar, 68 + }, 69 69 }, 70 - }, 71 - onPost: () => 72 - queryClient.invalidateQueries({ 73 - queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), 74 - }), 75 - }) 76 - }, [openComposer, queryClient, resolvedUri]) 70 + onPost: () => 71 + queryClient.invalidateQueries({ 72 + queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''), 73 + }), 74 + }) 75 + }, [openComposer, queryClient, resolvedUri]) 77 76 78 - return ( 79 - <View style={s.hContentRegion}> 80 - {isMobile && <ViewHeader title={_(msg`Post`)} />} 81 - <View style={s.flex1}> 82 - {uriError ? ( 83 - <CenteredView> 84 - <ErrorMessage message={String(uriError)} /> 85 - </CenteredView> 86 - ) : ( 87 - <PostThreadComponent 88 - uri={resolvedUri?.uri} 89 - onPressReply={onPressReply} 90 - /> 77 + return ( 78 + <View style={s.hContentRegion}> 79 + {isMobile && <ViewHeader title={_(msg`Post`)} />} 80 + <View style={s.flex1}> 81 + {uriError ? ( 82 + <CenteredView> 83 + <ErrorMessage message={String(uriError)} /> 84 + </CenteredView> 85 + ) : ( 86 + <PostThreadComponent 87 + uri={resolvedUri?.uri} 88 + onPressReply={onPressReply} 89 + /> 90 + )} 91 + </View> 92 + {isMobile && ( 93 + <Animated.View 94 + style={[ 95 + styles.prompt, 96 + fabMinimalShellTransform, 97 + { 98 + bottom: clamp(safeAreaInsets.bottom, 15, 30), 99 + }, 100 + ]}> 101 + <ComposePrompt onPressCompose={onPressReply} /> 102 + </Animated.View> 91 103 )} 92 104 </View> 93 - {isMobile && ( 94 - <Animated.View 95 - style={[ 96 - styles.prompt, 97 - fabMinimalShellTransform, 98 - { 99 - bottom: clamp(safeAreaInsets.bottom, 15, 30), 100 - }, 101 - ]}> 102 - <ComposePrompt onPressCompose={onPressReply} /> 103 - </Animated.View> 104 - )} 105 - </View> 106 - ) 107 - }) 105 + ) 106 + }, 107 + {isPublic: true}, 108 + ) 108 109 109 110 const styles = StyleSheet.create({ 110 111 prompt: {
+67 -64
src/view/screens/Profile.tsx
··· 43 43 } 44 44 45 45 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> 46 - export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({ 47 - route, 48 - }: Props) { 49 - const {currentAccount} = useSession() 50 - const name = 51 - route.params.name === 'me' ? currentAccount?.did : route.params.name 52 - const moderationOpts = useModerationOpts() 53 - const { 54 - data: resolvedDid, 55 - error: resolveError, 56 - refetch: refetchDid, 57 - isFetching: isFetchingDid, 58 - } = useResolveDidQuery(name) 59 - const { 60 - data: profile, 61 - dataUpdatedAt, 62 - error: profileError, 63 - refetch: refetchProfile, 64 - isFetching: isFetchingProfile, 65 - } = useProfileQuery({ 66 - did: resolvedDid?.did, 67 - }) 46 + export const ProfileScreen = withAuthRequired( 47 + function ProfileScreenImpl({route}: Props) { 48 + const {currentAccount} = useSession() 49 + const name = 50 + route.params.name === 'me' ? currentAccount?.did : route.params.name 51 + const moderationOpts = useModerationOpts() 52 + const { 53 + data: resolvedDid, 54 + error: resolveError, 55 + refetch: refetchDid, 56 + isFetching: isFetchingDid, 57 + } = useResolveDidQuery(name) 58 + const { 59 + data: profile, 60 + dataUpdatedAt, 61 + error: profileError, 62 + refetch: refetchProfile, 63 + isFetching: isFetchingProfile, 64 + } = useProfileQuery({ 65 + did: resolvedDid?.did, 66 + }) 68 67 69 - const onPressTryAgain = React.useCallback(() => { 70 - if (resolveError) { 71 - refetchDid() 72 - } else { 73 - refetchProfile() 68 + const onPressTryAgain = React.useCallback(() => { 69 + if (resolveError) { 70 + refetchDid() 71 + } else { 72 + refetchProfile() 73 + } 74 + }, [resolveError, refetchDid, refetchProfile]) 75 + 76 + if (isFetchingDid || isFetchingProfile || !moderationOpts) { 77 + return ( 78 + <CenteredView> 79 + <ProfileHeader 80 + profile={null} 81 + moderation={null} 82 + isProfilePreview={true} 83 + /> 84 + </CenteredView> 85 + ) 86 + } 87 + if (resolveError || profileError) { 88 + return ( 89 + <CenteredView> 90 + <ErrorScreen 91 + testID="profileErrorScreen" 92 + title="Oops!" 93 + message={cleanError(resolveError || profileError)} 94 + onPressTryAgain={onPressTryAgain} 95 + /> 96 + </CenteredView> 97 + ) 74 98 } 75 - }, [resolveError, refetchDid, refetchProfile]) 76 - 77 - if (isFetchingDid || isFetchingProfile || !moderationOpts) { 78 - return ( 79 - <CenteredView> 80 - <ProfileHeader 81 - profile={null} 82 - moderation={null} 83 - isProfilePreview={true} 99 + if (profile && moderationOpts) { 100 + return ( 101 + <ProfileScreenLoaded 102 + profile={profile} 103 + dataUpdatedAt={dataUpdatedAt} 104 + moderationOpts={moderationOpts} 105 + hideBackButton={!!route.params.hideBackButton} 84 106 /> 85 - </CenteredView> 86 - ) 87 - } 88 - if (resolveError || profileError) { 107 + ) 108 + } 109 + // should never happen 89 110 return ( 90 111 <CenteredView> 91 112 <ErrorScreen 92 113 testID="profileErrorScreen" 93 114 title="Oops!" 94 - message={cleanError(resolveError || profileError)} 115 + message="Something went wrong and we're not sure what." 95 116 onPressTryAgain={onPressTryAgain} 96 117 /> 97 118 </CenteredView> 98 119 ) 99 - } 100 - if (profile && moderationOpts) { 101 - return ( 102 - <ProfileScreenLoaded 103 - profile={profile} 104 - dataUpdatedAt={dataUpdatedAt} 105 - moderationOpts={moderationOpts} 106 - hideBackButton={!!route.params.hideBackButton} 107 - /> 108 - ) 109 - } 110 - // should never happen 111 - return ( 112 - <CenteredView> 113 - <ErrorScreen 114 - testID="profileErrorScreen" 115 - title="Oops!" 116 - message="Something went wrong and we're not sure what." 117 - onPressTryAgain={onPressTryAgain} 118 - /> 119 - </CenteredView> 120 - ) 121 - }) 120 + }, 121 + { 122 + isPublic: true, 123 + }, 124 + ) 122 125 123 126 function ProfileScreenLoaded({ 124 127 profile: profileUnshadowed,
+3
src/view/screens/ProfileFeed.tsx
··· 129 129 </CenteredView> 130 130 ) 131 131 }, 132 + { 133 + isPublic: true, 134 + }, 132 135 ) 133 136 134 137 function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
+20 -17
src/view/screens/ProfileFeedLikedBy.tsx
··· 11 11 import {msg} from '@lingui/macro' 12 12 13 13 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'> 14 - export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => { 15 - const setMinimalShellMode = useSetMinimalShellMode() 16 - const {name, rkey} = route.params 17 - const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) 18 - const {_} = useLingui() 14 + export const ProfileFeedLikedByScreen = withAuthRequired( 15 + ({route}: Props) => { 16 + const setMinimalShellMode = useSetMinimalShellMode() 17 + const {name, rkey} = route.params 18 + const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) 19 + const {_} = useLingui() 19 20 20 - useFocusEffect( 21 - React.useCallback(() => { 22 - setMinimalShellMode(false) 23 - }, [setMinimalShellMode]), 24 - ) 21 + useFocusEffect( 22 + React.useCallback(() => { 23 + setMinimalShellMode(false) 24 + }, [setMinimalShellMode]), 25 + ) 25 26 26 - return ( 27 - <View> 28 - <ViewHeader title={_(msg`Liked by`)} /> 29 - <PostLikedByComponent uri={uri} /> 30 - </View> 31 - ) 32 - }) 27 + return ( 28 + <View> 29 + <ViewHeader title={_(msg`Liked by`)} /> 30 + <PostLikedByComponent uri={uri} /> 31 + </View> 32 + ) 33 + }, 34 + {isPublic: true}, 35 + )
+19 -16
src/view/screens/ProfileFollowers.tsx
··· 10 10 import {msg} from '@lingui/macro' 11 11 12 12 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'> 13 - export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => { 14 - const {name} = route.params 15 - const setMinimalShellMode = useSetMinimalShellMode() 16 - const {_} = useLingui() 13 + export const ProfileFollowersScreen = withAuthRequired( 14 + ({route}: Props) => { 15 + const {name} = route.params 16 + const setMinimalShellMode = useSetMinimalShellMode() 17 + const {_} = useLingui() 17 18 18 - useFocusEffect( 19 - React.useCallback(() => { 20 - setMinimalShellMode(false) 21 - }, [setMinimalShellMode]), 22 - ) 19 + useFocusEffect( 20 + React.useCallback(() => { 21 + setMinimalShellMode(false) 22 + }, [setMinimalShellMode]), 23 + ) 23 24 24 - return ( 25 - <View> 26 - <ViewHeader title={_(msg`Followers`)} /> 27 - <ProfileFollowersComponent name={name} /> 28 - </View> 29 - ) 30 - }) 25 + return ( 26 + <View> 27 + <ViewHeader title={_(msg`Followers`)} /> 28 + <ProfileFollowersComponent name={name} /> 29 + </View> 30 + ) 31 + }, 32 + {isPublic: true}, 33 + )
+19 -16
src/view/screens/ProfileFollows.tsx
··· 10 10 import {msg} from '@lingui/macro' 11 11 12 12 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'> 13 - export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => { 14 - const {name} = route.params 15 - const setMinimalShellMode = useSetMinimalShellMode() 16 - const {_} = useLingui() 13 + export const ProfileFollowsScreen = withAuthRequired( 14 + ({route}: Props) => { 15 + const {name} = route.params 16 + const setMinimalShellMode = useSetMinimalShellMode() 17 + const {_} = useLingui() 17 18 18 - useFocusEffect( 19 - React.useCallback(() => { 20 - setMinimalShellMode(false) 21 - }, [setMinimalShellMode]), 22 - ) 19 + useFocusEffect( 20 + React.useCallback(() => { 21 + setMinimalShellMode(false) 22 + }, [setMinimalShellMode]), 23 + ) 23 24 24 - return ( 25 - <View> 26 - <ViewHeader title={_(msg`Following`)} /> 27 - <ProfileFollowsComponent name={name} /> 28 - </View> 29 - ) 30 - }) 25 + return ( 26 + <View> 27 + <ViewHeader title={_(msg`Following`)} /> 28 + <ProfileFollowsComponent name={name} /> 29 + </View> 30 + ) 31 + }, 32 + {isPublic: true}, 33 + )
+9 -7
src/view/screens/SavedFeeds.tsx
··· 33 33 usePinFeedMutation, 34 34 useUnpinFeedMutation, 35 35 useSetSaveFeedsMutation, 36 - usePreferencesQueryKey, 36 + preferencesQueryKey, 37 37 UsePreferencesQueryResponse, 38 38 } from '#/state/queries/preferences' 39 39 ··· 182 182 const onPressUp = React.useCallback(async () => { 183 183 if (!isPinned) return 184 184 185 - const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>( 186 - usePreferencesQueryKey, 187 - )?.feeds 185 + const feeds = 186 + queryClient.getQueryData<UsePreferencesQueryResponse>( 187 + preferencesQueryKey, 188 + )?.feeds 188 189 const pinned = feeds?.pinned ?? [] 189 190 const index = pinned.indexOf(feedUri) 190 191 ··· 206 207 const onPressDown = React.useCallback(async () => { 207 208 if (!isPinned) return 208 209 209 - const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>( 210 - usePreferencesQueryKey, 211 - )?.feeds 210 + const feeds = 211 + queryClient.getQueryData<UsePreferencesQueryResponse>( 212 + preferencesQueryKey, 213 + )?.feeds 212 214 const pinned = feeds?.pinned ?? [] 213 215 const index = pinned.indexOf(feedUri) 214 216