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 importzio/stable
parent
71b59021b9
commit
f18b9b32b0
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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={`${
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
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,
|
||||
])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
let slices: FlatlistSlice[] = []
|
||||
const items = React.useMemo(() => {
|
||||
let slices: FlatlistSlice[] = []
|
||||
|
||||
slices.push({
|
||||
key: 'savedFeedsHeader',
|
||||
type: 'savedFeedsHeader',
|
||||
})
|
||||
|
||||
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
|
||||
if (preferences?.feeds?.saved.length === 0) {
|
||||
slices.push({
|
||||
key: 'savedFeedNoResults',
|
||||
type: 'savedFeedNoResults',
|
||||
})
|
||||
} else {
|
||||
const {saved, pinned} = preferences.feeds
|
||||
|
||||
slices = slices.concat(
|
||||
pinned.map(uri => ({
|
||||
key: `savedFeed:${uri}`,
|
||||
type: 'savedFeed',
|
||||
feedUri: uri,
|
||||
})),
|
||||
)
|
||||
|
||||
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.push({
|
||||
key: 'popularFeedsHeader',
|
||||
type: 'popularFeedsHeader',
|
||||
})
|
||||
|
||||
if (popularFeedsError || searchError) {
|
||||
slices.push({
|
||||
key: 'popularFeedsError',
|
||||
type: 'error',
|
||||
error: cleanError(
|
||||
popularFeedsError?.toString() ?? searchError?.toString() ?? '',
|
||||
),
|
||||
})
|
||||
} else {
|
||||
if (isUserSearching) {
|
||||
if (isSearchPending || !searchResults) {
|
||||
slices.push({
|
||||
key: 'popularFeedsLoading',
|
||||
type: 'popularFeedsLoading',
|
||||
})
|
||||
} else {
|
||||
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,
|
||||
})),
|
||||
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() ?? '',
|
||||
),
|
||||
})
|
||||
} else {
|
||||
if (isPopularFeedsFetching && !popularFeeds?.pages) {
|
||||
slices.push({
|
||||
key: 'popularFeedsLoading',
|
||||
type: 'popularFeedsLoading',
|
||||
})
|
||||
} else {
|
||||
if (
|
||||
!popularFeeds?.pages ||
|
||||
popularFeeds?.pages[0]?.feeds?.length === 0
|
||||
) {
|
||||
if (isUserSearching) {
|
||||
if (isSearchPending || !searchResults) {
|
||||
slices.push({
|
||||
key: 'popularFeedsNoResults',
|
||||
type: 'popularFeedsNoResults',
|
||||
key: 'popularFeedsLoading',
|
||||
type: 'popularFeedsLoading',
|
||||
})
|
||||
} else {
|
||||
for (const page of popularFeeds.pages || []) {
|
||||
if (!searchResults || searchResults?.length === 0) {
|
||||
slices.push({
|
||||
key: 'popularFeedsNoResults',
|
||||
type: 'popularFeedsNoResults',
|
||||
})
|
||||
} else {
|
||||
slices = slices.concat(
|
||||
page.feeds
|
||||
.filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
|
||||
.map(feed => ({
|
||||
key: `popularFeed:${feed.uri}`,
|
||||
type: 'popularFeed',
|
||||
feedUri: feed.uri,
|
||||
})),
|
||||
searchResults.map(feed => ({
|
||||
key: `popularFeed:${feed.uri}`,
|
||||
type: 'popularFeed',
|
||||
feedUri: feed.uri,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
if (isPopularFeedsFetchingNextPage) {
|
||||
}
|
||||
} else {
|
||||
if (isPopularFeedsFetching && !popularFeeds?.pages) {
|
||||
slices.push({
|
||||
key: 'popularFeedsLoading',
|
||||
type: 'popularFeedsLoading',
|
||||
})
|
||||
} else {
|
||||
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={s.p10}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
} 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,
|
||||
pal.border,
|
||||
{
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
]}>
|
||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
||||
<Trans>My 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 && (
|
||||
<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>
|
||||
<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={{
|
||||
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>
|
||||
return null
|
||||
},
|
||||
[
|
||||
_,
|
||||
isMobile,
|
||||
pal,
|
||||
query,
|
||||
onChangeQuery,
|
||||
onPressCancelSearch,
|
||||
onSubmitQuery,
|
||||
],
|
||||
)
|
||||
|
||||
{!isMobile && (
|
||||
<SearchInput
|
||||
query={query}
|
||||
onChangeQuery={onChangeQuery}
|
||||
onPressCancelSearch={onPressCancelSearch}
|
||||
onSubmitQuery={onSubmitQuery}
|
||||
style={{flex: 1, maxWidth: 250}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{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 />}
|
||||
|
||||
<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
|
||||
/>
|
||||
)}
|
||||
|
||||
{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
|
||||
/>
|
||||
|
||||
<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')
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
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}
|
||||
/>
|
||||
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])
|
||||
|
||||
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: {
|
||||
|
|
|
@ -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()
|
||||
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}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
}, [resolveError, refetchDid, refetchProfile])
|
||||
|
||||
if (isFetchingDid || isFetchingProfile || !moderationOpts) {
|
||||
return (
|
||||
<CenteredView>
|
||||
<ProfileHeader
|
||||
profile={null}
|
||||
moderation={null}
|
||||
isProfilePreview={true}
|
||||
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,
|
||||
|
|
|
@ -129,6 +129,9 @@ export const ProfileFeedScreen = withAuthRequired(
|
|||
</CenteredView>
|
||||
)
|
||||
},
|
||||
{
|
||||
isPublic: true,
|
||||
},
|
||||
)
|
||||
|
||||
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue