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
This commit is contained in:
parent
71b59021b9
commit
f18b9b32b0
25 changed files with 1026 additions and 755 deletions
|
@ -2,6 +2,7 @@ import {
|
||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
ThreadViewPreferences,
|
ThreadViewPreferences,
|
||||||
} from '#/state/queries/preferences/types'
|
} from '#/state/queries/preferences/types'
|
||||||
|
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||||
|
|
||||||
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
||||||
{
|
{
|
||||||
|
@ -25,3 +26,26 @@ export const DEFAULT_PROD_FEEDS = {
|
||||||
pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
||||||
saved: [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 {
|
import {
|
||||||
DEFAULT_HOME_FEED_PREFS,
|
DEFAULT_HOME_FEED_PREFS,
|
||||||
DEFAULT_THREAD_VIEW_PREFS,
|
DEFAULT_THREAD_VIEW_PREFS,
|
||||||
|
DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||||
} from '#/state/queries/preferences/const'
|
} from '#/state/queries/preferences/const'
|
||||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||||
import {STALE} from '#/state/queries'
|
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/moderation'
|
||||||
export * from '#/state/queries/preferences/const'
|
export * from '#/state/queries/preferences/const'
|
||||||
|
|
||||||
export const usePreferencesQueryKey = ['getPreferences']
|
export const preferencesQueryKey = ['getPreferences']
|
||||||
|
|
||||||
export function usePreferencesQuery() {
|
export function usePreferencesQuery() {
|
||||||
const {hasSession} = useSession()
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: hasSession,
|
|
||||||
staleTime: STALE.MINUTES.ONE,
|
staleTime: STALE.MINUTES.ONE,
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await getAgent().getPreferences()
|
const agent = getAgent()
|
||||||
const preferences: UsePreferencesQueryResponse = {
|
|
||||||
...res,
|
if (agent.session?.did === undefined) {
|
||||||
feeds: {
|
return DEFAULT_LOGGED_OUT_PREFERENCES
|
||||||
saved: res.feeds?.saved || [],
|
} else {
|
||||||
pinned: res.feeds?.pinned || [],
|
const res = await agent.getPreferences()
|
||||||
unpinned:
|
const preferences: UsePreferencesQueryResponse = {
|
||||||
res.feeds.saved?.filter(f => {
|
...res,
|
||||||
return !res.feeds.pinned?.includes(f)
|
feeds: {
|
||||||
}) || [],
|
saved: res.feeds?.saved || [],
|
||||||
},
|
pinned: res.feeds?.pinned || [],
|
||||||
// labels are undefined until set by user
|
unpinned:
|
||||||
contentLabels: {
|
res.feeds.saved?.filter(f => {
|
||||||
nsfw: temp__migrateLabelPref(
|
return !res.feeds.pinned?.includes(f)
|
||||||
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
}) || [],
|
||||||
),
|
},
|
||||||
nudity: temp__migrateLabelPref(
|
// labels are undefined until set by user
|
||||||
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
contentLabels: {
|
||||||
),
|
nsfw: temp__migrateLabelPref(
|
||||||
suggestive: temp__migrateLabelPref(
|
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
||||||
res.contentLabels?.suggestive ||
|
),
|
||||||
DEFAULT_LABEL_PREFERENCES.suggestive,
|
nudity: temp__migrateLabelPref(
|
||||||
),
|
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
||||||
gore: temp__migrateLabelPref(
|
),
|
||||||
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
suggestive: temp__migrateLabelPref(
|
||||||
),
|
res.contentLabels?.suggestive ||
|
||||||
hate: temp__migrateLabelPref(
|
DEFAULT_LABEL_PREFERENCES.suggestive,
|
||||||
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
),
|
||||||
),
|
gore: temp__migrateLabelPref(
|
||||||
spam: temp__migrateLabelPref(
|
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
||||||
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
),
|
||||||
),
|
hate: temp__migrateLabelPref(
|
||||||
impersonation: temp__migrateLabelPref(
|
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
||||||
res.contentLabels?.impersonation ||
|
),
|
||||||
DEFAULT_LABEL_PREFERENCES.impersonation,
|
spam: temp__migrateLabelPref(
|
||||||
),
|
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
||||||
},
|
),
|
||||||
feedViewPrefs: {
|
impersonation: temp__migrateLabelPref(
|
||||||
...DEFAULT_HOME_FEED_PREFS,
|
res.contentLabels?.impersonation ||
|
||||||
...(res.feedViewPrefs.home || {}),
|
DEFAULT_LABEL_PREFERENCES.impersonation,
|
||||||
},
|
),
|
||||||
threadViewPrefs: {
|
},
|
||||||
...DEFAULT_THREAD_VIEW_PREFS,
|
feedViewPrefs: {
|
||||||
...(res.threadViewPrefs ?? {}),
|
...DEFAULT_HOME_FEED_PREFS,
|
||||||
},
|
...(res.feedViewPrefs.home || {}),
|
||||||
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
|
},
|
||||||
|
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: []})
|
await getAgent().app.bsky.actor.putPreferences({preferences: []})
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -125,7 +130,7 @@ export function usePreferencesSetContentLabelMutation() {
|
||||||
await getAgent().setContentLabelPref(labelGroup, visibility)
|
await getAgent().setContentLabelPref(labelGroup, visibility)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -139,7 +144,7 @@ export function usePreferencesSetAdultContentMutation() {
|
||||||
await getAgent().setAdultContentEnabled(enabled)
|
await getAgent().setAdultContentEnabled(enabled)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -153,7 +158,7 @@ export function usePreferencesSetBirthDateMutation() {
|
||||||
await getAgent().setPersonalDetails({birthDate})
|
await getAgent().setPersonalDetails({birthDate})
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -167,7 +172,7 @@ export function useSetFeedViewPreferencesMutation() {
|
||||||
await getAgent().setFeedViewPrefs('home', prefs)
|
await getAgent().setFeedViewPrefs('home', prefs)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -181,7 +186,7 @@ export function useSetThreadViewPreferencesMutation() {
|
||||||
await getAgent().setThreadViewPrefs(prefs)
|
await getAgent().setThreadViewPrefs(prefs)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -199,7 +204,7 @@ export function useSetSaveFeedsMutation() {
|
||||||
await getAgent().setSavedFeeds(saved, pinned)
|
await getAgent().setSavedFeeds(saved, pinned)
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -214,7 +219,7 @@ export function useSaveFeedMutation() {
|
||||||
track('CustomFeed:Save')
|
track('CustomFeed:Save')
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -229,7 +234,7 @@ export function useRemoveFeedMutation() {
|
||||||
track('CustomFeed:Unsave')
|
track('CustomFeed:Unsave')
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -244,7 +249,7 @@ export function usePinFeedMutation() {
|
||||||
track('CustomFeed:Pin', {uri})
|
track('CustomFeed:Pin', {uri})
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -259,7 +264,7 @@ export function useUnpinFeedMutation() {
|
||||||
track('CustomFeed:Unpin', {uri})
|
track('CustomFeed:Unpin', {uri})
|
||||||
// triggers a refetch
|
// triggers a refetch
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: usePreferencesQueryKey,
|
queryKey: preferencesQueryKey,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,6 +34,24 @@ export const DEFAULT_LABEL_PREFERENCES: Record<
|
||||||
impersonation: 'hide',
|
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 = {
|
export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
|
||||||
id: 'illegal',
|
id: 'illegal',
|
||||||
title: 'Illegal Content',
|
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
|
sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string
|
||||||
lab_treeViewEnabled?: boolean
|
lab_treeViewEnabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as persisted from '#/state/persisted'
|
||||||
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
||||||
import {IS_PROD} from '#/lib/constants'
|
import {IS_PROD} from '#/lib/constants'
|
||||||
import {emitSessionLoaded, emitSessionDropped} from '../events'
|
import {emitSessionLoaded, emitSessionDropped} from '../events'
|
||||||
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
|
|
||||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||||
|
|
||||||
|
@ -515,3 +516,19 @@ export function useSession() {
|
||||||
export function useSessionApi() {
|
export function useSessionApi() {
|
||||||
return React.useContext(ApiContext)
|
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 OnboardingProvider} from './onboarding'
|
||||||
import {Provider as ComposerProvider} from './composer'
|
import {Provider as ComposerProvider} from './composer'
|
||||||
import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
|
import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
|
||||||
|
import {Provider as LoggedOutViewProvider} from './logged-out'
|
||||||
|
|
||||||
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
|
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
|
||||||
export {
|
export {
|
||||||
|
@ -22,19 +23,23 @@ export {useTickEveryMinute} from './tick-every-minute'
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
<ShellLayoutProvder>
|
<ShellLayoutProvder>
|
||||||
<DrawerOpenProvider>
|
<LoggedOutViewProvider>
|
||||||
<DrawerSwipableProvider>
|
<DrawerOpenProvider>
|
||||||
<MinimalModeProvider>
|
<DrawerSwipableProvider>
|
||||||
<ColorModeProvider>
|
<MinimalModeProvider>
|
||||||
<OnboardingProvider>
|
<ColorModeProvider>
|
||||||
<ComposerProvider>
|
<OnboardingProvider>
|
||||||
<TickEveryMinuteProvider>{children}</TickEveryMinuteProvider>
|
<ComposerProvider>
|
||||||
</ComposerProvider>
|
<TickEveryMinuteProvider>
|
||||||
</OnboardingProvider>
|
{children}
|
||||||
</ColorModeProvider>
|
</TickEveryMinuteProvider>
|
||||||
</MinimalModeProvider>
|
</ComposerProvider>
|
||||||
</DrawerSwipableProvider>
|
</OnboardingProvider>
|
||||||
</DrawerOpenProvider>
|
</ColorModeProvider>
|
||||||
|
</MinimalModeProvider>
|
||||||
|
</DrawerSwipableProvider>
|
||||||
|
</DrawerOpenProvider>
|
||||||
|
</LoggedOutViewProvider>
|
||||||
</ShellLayoutProvder>
|
</ShellLayoutProvder>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
37
src/state/shell/logged-out.tsx
Normal file
37
src/state/shell/logged-out.tsx
Normal file
|
@ -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,
|
S_CreateAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoggedOut() {
|
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {screen} = useAnalytics()
|
const {screen} = useAnalytics()
|
||||||
|
@ -31,6 +31,7 @@ export function LoggedOut() {
|
||||||
if (screenState === ScreenState.S_LoginOrCreateAccount) {
|
if (screenState === ScreenState.S_LoginOrCreateAccount) {
|
||||||
return (
|
return (
|
||||||
<SplashScreen
|
<SplashScreen
|
||||||
|
onDismiss={onDismiss}
|
||||||
onPressSignin={() => setScreenState(ScreenState.S_Login)}
|
onPressSignin={() => setScreenState(ScreenState.S_Login)}
|
||||||
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
|
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import React from 'react'
|
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 {Text} from 'view/com/util/text/Text'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
|
@ -9,9 +16,11 @@ import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
|
onDismiss,
|
||||||
onPressSignin,
|
onPressSignin,
|
||||||
onPressCreateAccount,
|
onPressCreateAccount,
|
||||||
}: {
|
}: {
|
||||||
|
onDismiss?: () => void
|
||||||
onPressSignin: () => void
|
onPressSignin: () => void
|
||||||
onPressCreateAccount: () => void
|
onPressCreateAccount: () => void
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -20,6 +29,27 @@ export const SplashScreen = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[styles.container, pal.view]}>
|
<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}>
|
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={styles.hero}>
|
<View style={styles.hero}>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
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 {Text} from 'view/com/util/text/Text'
|
||||||
import {TextLink} from '../util/Link'
|
import {TextLink} from '../util/Link'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
|
@ -11,9 +12,11 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
|
onDismiss,
|
||||||
onPressSignin,
|
onPressSignin,
|
||||||
onPressCreateAccount,
|
onPressCreateAccount,
|
||||||
}: {
|
}: {
|
||||||
|
onDismiss?: () => void
|
||||||
onPressSignin: () => void
|
onPressSignin: () => void
|
||||||
onPressCreateAccount: () => void
|
onPressCreateAccount: () => void
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -23,47 +26,70 @@ export const SplashScreen = ({
|
||||||
const isMobileWeb = isWeb && isTabletOrMobile
|
const isMobileWeb = isWeb && isTabletOrMobile
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[styles.container, pal.view]}>
|
<>
|
||||||
<View
|
{onDismiss && (
|
||||||
testID="noSessionView"
|
<Pressable
|
||||||
style={[
|
accessibilityRole="button"
|
||||||
styles.containerInner,
|
style={{
|
||||||
isMobileWeb && styles.containerInnerMobile,
|
position: 'absolute',
|
||||||
pal.border,
|
top: 20,
|
||||||
]}>
|
right: 20,
|
||||||
<ErrorBoundary>
|
padding: 20,
|
||||||
<Text style={isMobileWeb ? styles.titleMobile : styles.title}>
|
zIndex: 100,
|
||||||
Bluesky
|
}}
|
||||||
</Text>
|
onPress={onDismiss}>
|
||||||
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
|
<FontAwesomeIcon
|
||||||
See what's next
|
icon="x"
|
||||||
</Text>
|
size={24}
|
||||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
style={{
|
||||||
<TouchableOpacity
|
color: String(pal.text.color),
|
||||||
testID="createAccountButton"
|
}}
|
||||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
/>
|
||||||
onPress={onPressCreateAccount}
|
</Pressable>
|
||||||
// TODO: web accessibility
|
)}
|
||||||
accessibilityRole="button">
|
|
||||||
<Text style={[s.white, styles.btnLabel]}>
|
<CenteredView style={[styles.container, pal.view]}>
|
||||||
Create a new account
|
<View
|
||||||
</Text>
|
testID="noSessionView"
|
||||||
</TouchableOpacity>
|
style={[
|
||||||
<TouchableOpacity
|
styles.containerInner,
|
||||||
testID="signInButton"
|
isMobileWeb && styles.containerInnerMobile,
|
||||||
style={[styles.btn, pal.btn]}
|
pal.border,
|
||||||
onPress={onPressSignin}
|
]}>
|
||||||
// TODO: web accessibility
|
<ErrorBoundary>
|
||||||
accessibilityRole="button">
|
<Text style={isMobileWeb ? styles.titleMobile : styles.title}>
|
||||||
<Text style={[pal.text, styles.btnLabel]}>
|
Bluesky
|
||||||
<Trans>Sign In</Trans>
|
</Text>
|
||||||
</Text>
|
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
|
||||||
</TouchableOpacity>
|
See what's next
|
||||||
</View>
|
</Text>
|
||||||
</ErrorBoundary>
|
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||||
</View>
|
<TouchableOpacity
|
||||||
<Footer styles={styles} />
|
testID="createAccountButton"
|
||||||
</CenteredView>
|
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 {STATUS_PAGE_URL} from 'lib/constants'
|
||||||
import {useOnboardingState} from '#/state/shell'
|
import {useOnboardingState} from '#/state/shell'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {
|
||||||
|
useLoggedOutView,
|
||||||
|
useLoggedOutViewControls,
|
||||||
|
} from '#/state/shell/logged-out'
|
||||||
|
import {IS_PROD} from '#/env'
|
||||||
|
|
||||||
export const withAuthRequired = <P extends object>(
|
export const withAuthRequired = <P extends object>(
|
||||||
Component: React.ComponentType<P>,
|
Component: React.ComponentType<P>,
|
||||||
|
options: {
|
||||||
|
isPublic?: boolean // TODO(pwi) need to enable in TF somehow
|
||||||
|
} = {},
|
||||||
): React.FC<P> =>
|
): React.FC<P> =>
|
||||||
function AuthRequired(props: P) {
|
function AuthRequired(props: P) {
|
||||||
const {isInitialLoad, hasSession} = useSession()
|
const {isInitialLoad, hasSession} = useSession()
|
||||||
const onboardingState = useOnboardingState()
|
const onboardingState = useOnboardingState()
|
||||||
|
const {showLoggedOut} = useLoggedOutView()
|
||||||
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
|
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
}
|
}
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
return <LoggedOut />
|
if (showLoggedOut) {
|
||||||
|
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
|
||||||
|
} else if (!options?.isPublic || IS_PROD) {
|
||||||
|
return <LoggedOut />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (onboardingState.isActive) {
|
if (onboardingState.isActive) {
|
||||||
return <Onboarding />
|
return <Onboarding />
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
} from '#/state/queries/post'
|
} from '#/state/queries/post'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
|
import {useRequireAuth} from '#/state/session'
|
||||||
|
|
||||||
export function PostCtrls({
|
export function PostCtrls({
|
||||||
big,
|
big,
|
||||||
|
@ -46,6 +47,7 @@ export function PostCtrls({
|
||||||
const postUnlikeMutation = usePostUnlikeMutation()
|
const postUnlikeMutation = usePostUnlikeMutation()
|
||||||
const postRepostMutation = usePostRepostMutation()
|
const postRepostMutation = usePostRepostMutation()
|
||||||
const postUnrepostMutation = usePostUnrepostMutation()
|
const postUnrepostMutation = usePostUnrepostMutation()
|
||||||
|
const requireAuth = useRequireAuth()
|
||||||
|
|
||||||
const defaultCtrlColor = React.useMemo(
|
const defaultCtrlColor = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -107,7 +109,9 @@ export function PostCtrls({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="replyBtn"
|
testID="replyBtn"
|
||||||
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||||
onPress={onPressReply}
|
onPress={() => {
|
||||||
|
requireAuth(() => onPressReply())
|
||||||
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||||
post.replyCount === 1 ? 'reply' : 'replies'
|
post.replyCount === 1 ? 'reply' : 'replies'
|
||||||
|
@ -135,7 +139,9 @@ export function PostCtrls({
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="likeBtn"
|
testID="likeBtn"
|
||||||
style={[styles.ctrl, !big && styles.ctrlPad]}
|
style={[styles.ctrl, !big && styles.ctrlPad]}
|
||||||
onPress={onPressToggleLike}
|
onPress={() => {
|
||||||
|
requireAuth(() => onPressToggleLike())
|
||||||
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
|
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
|
||||||
post.likeCount
|
post.likeCount
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {Text} from '../text/Text'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {useRequireAuth} from '#/state/session'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
|
@ -25,6 +26,7 @@ export const RepostButton = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
const requireAuth = useRequireAuth()
|
||||||
|
|
||||||
const defaultControlColor = React.useMemo(
|
const defaultControlColor = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
@ -45,7 +47,9 @@ export const RepostButton = ({
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="repostBtn"
|
testID="repostBtn"
|
||||||
onPress={onPressToggleRepostWrapper}
|
onPress={() => {
|
||||||
|
requireAuth(() => onPressToggleRepostWrapper())
|
||||||
|
}}
|
||||||
style={[styles.control, !big && styles.controlPad]}
|
style={[styles.control, !big && styles.controlPad]}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`${
|
accessibilityLabel={`${
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
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 {RepostIcon} from 'lib/icons'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
|
@ -12,6 +12,8 @@ import {
|
||||||
import {EventStopper} from '../EventStopper'
|
import {EventStopper} from '../EventStopper'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useRequireAuth} from '#/state/session'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
|
@ -31,6 +33,8 @@ export const RepostButton = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const {hasSession} = useSession()
|
||||||
|
const requireAuth = useRequireAuth()
|
||||||
|
|
||||||
const defaultControlColor = React.useMemo(
|
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>
|
<EventStopper>
|
||||||
<NativeDropdown
|
<NativeDropdown
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
accessibilityLabel={_(msg`Repost or quote post`)}
|
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||||
accessibilityHint="">
|
accessibilityHint="">
|
||||||
<View
|
{inner}
|
||||||
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>
|
|
||||||
</NativeDropdown>
|
</NativeDropdown>
|
||||||
</EventStopper>
|
</EventStopper>
|
||||||
|
) : (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => {
|
||||||
|
requireAuth(() => {})
|
||||||
|
}}
|
||||||
|
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||||
|
accessibilityHint="">
|
||||||
|
{inner}
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,426 +87,429 @@ type FlatlistSlice =
|
||||||
key: string
|
key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FeedsScreen = withAuthRequired(function FeedsScreenImpl(
|
export const FeedsScreen = withAuthRequired(
|
||||||
_props: Props,
|
function FeedsScreenImpl(_props: Props) {
|
||||||
) {
|
const pal = usePalette('default')
|
||||||
const pal = usePalette('default')
|
const {openComposer} = useComposerControls()
|
||||||
const {openComposer} = useComposerControls()
|
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
const [query, setQuery] = React.useState('')
|
||||||
const [query, setQuery] = React.useState('')
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
const [isPTR, setIsPTR] = React.useState(false)
|
const {
|
||||||
const {
|
data: preferences,
|
||||||
data: preferences,
|
isLoading: isPreferencesLoading,
|
||||||
isLoading: isPreferencesLoading,
|
error: preferencesError,
|
||||||
error: preferencesError,
|
} = usePreferencesQuery()
|
||||||
} = usePreferencesQuery()
|
const {
|
||||||
const {
|
data: popularFeeds,
|
||||||
data: popularFeeds,
|
isFetching: isPopularFeedsFetching,
|
||||||
isFetching: isPopularFeedsFetching,
|
error: popularFeedsError,
|
||||||
error: popularFeedsError,
|
refetch: refetchPopularFeeds,
|
||||||
refetch: refetchPopularFeeds,
|
fetchNextPage: fetchNextPopularFeedsPage,
|
||||||
fetchNextPage: fetchNextPopularFeedsPage,
|
isFetchingNextPage: isPopularFeedsFetchingNextPage,
|
||||||
isFetchingNextPage: isPopularFeedsFetchingNextPage,
|
hasNextPage: hasNextPopularFeedsPage,
|
||||||
hasNextPage: hasNextPopularFeedsPage,
|
} = useGetPopularFeedsQuery()
|
||||||
} = useGetPopularFeedsQuery()
|
const {_} = useLingui()
|
||||||
const {_} = useLingui()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const {
|
||||||
const {
|
data: searchResults,
|
||||||
data: searchResults,
|
mutate: search,
|
||||||
mutate: search,
|
reset: resetSearch,
|
||||||
reset: resetSearch,
|
isPending: isSearchPending,
|
||||||
isPending: isSearchPending,
|
error: searchError,
|
||||||
error: searchError,
|
} = useSearchPopularFeedsMutation()
|
||||||
} = useSearchPopularFeedsMutation()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A search query is present. We may not have search results yet.
|
* A search query is present. We may not have search results yet.
|
||||||
*/
|
*/
|
||||||
const isUserSearching = query.length > 1
|
const isUserSearching = query.length > 1
|
||||||
const debouncedSearch = React.useMemo(
|
const debouncedSearch = React.useMemo(
|
||||||
() => debounce(q => search(q), 500), // debounce for 500ms
|
() => debounce(q => search(q), 500), // debounce for 500ms
|
||||||
[search],
|
[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
|
|
||||||
)
|
)
|
||||||
return
|
const onPressCompose = React.useCallback(() => {
|
||||||
fetchNextPopularFeedsPage()
|
openComposer({})
|
||||||
}, [
|
}, [openComposer])
|
||||||
isPopularFeedsFetching,
|
const onChangeQuery = React.useCallback(
|
||||||
isUserSearching,
|
(text: string) => {
|
||||||
popularFeedsError,
|
setQuery(text)
|
||||||
hasNextPopularFeedsPage,
|
if (text.length > 1) {
|
||||||
fetchNextPopularFeedsPage,
|
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(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const items = React.useMemo(() => {
|
const items = React.useMemo(() => {
|
||||||
let slices: FlatlistSlice[] = []
|
let slices: FlatlistSlice[] = []
|
||||||
|
|
||||||
slices.push({
|
|
||||||
key: 'savedFeedsHeader',
|
|
||||||
type: 'savedFeedsHeader',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (preferencesError) {
|
|
||||||
slices.push({
|
slices.push({
|
||||||
key: 'savedFeedsError',
|
key: 'savedFeedsHeader',
|
||||||
type: 'error',
|
type: 'savedFeedsHeader',
|
||||||
error: cleanError(preferencesError.toString()),
|
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
if (isPreferencesLoading || !preferences?.feeds?.saved) {
|
if (preferencesError) {
|
||||||
slices.push({
|
slices.push({
|
||||||
key: 'savedFeedsLoading',
|
key: 'savedFeedsError',
|
||||||
type: 'savedFeedsLoading',
|
type: 'error',
|
||||||
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
|
error: cleanError(preferencesError.toString()),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (preferences?.feeds?.saved.length === 0) {
|
if (isPreferencesLoading || !preferences?.feeds?.saved) {
|
||||||
slices.push({
|
slices.push({
|
||||||
key: 'savedFeedNoResults',
|
key: 'savedFeedsLoading',
|
||||||
type: 'savedFeedNoResults',
|
type: 'savedFeedsLoading',
|
||||||
|
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
|
||||||
})
|
})
|
||||||
} else {
|
} 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(
|
slices = slices.concat(
|
||||||
pinned.map(uri => ({
|
pinned.map(uri => ({
|
||||||
key: `savedFeed:${uri}`,
|
|
||||||
type: 'savedFeed',
|
|
||||||
feedUri: uri,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
slices = slices.concat(
|
|
||||||
saved
|
|
||||||
.filter(uri => !pinned.includes(uri))
|
|
||||||
.map(uri => ({
|
|
||||||
key: `savedFeed:${uri}`,
|
key: `savedFeed:${uri}`,
|
||||||
type: 'savedFeed',
|
type: 'savedFeed',
|
||||||
feedUri: uri,
|
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(
|
slices = slices.concat(
|
||||||
searchResults.map(feed => ({
|
saved
|
||||||
key: `popularFeed:${feed.uri}`,
|
.filter(uri => !pinned.includes(uri))
|
||||||
type: 'popularFeed',
|
.map(uri => ({
|
||||||
feedUri: feed.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 {
|
} else {
|
||||||
if (isPopularFeedsFetching && !popularFeeds?.pages) {
|
if (isUserSearching) {
|
||||||
slices.push({
|
if (isSearchPending || !searchResults) {
|
||||||
key: 'popularFeedsLoading',
|
|
||||||
type: 'popularFeedsLoading',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
!popularFeeds?.pages ||
|
|
||||||
popularFeeds?.pages[0]?.feeds?.length === 0
|
|
||||||
) {
|
|
||||||
slices.push({
|
slices.push({
|
||||||
key: 'popularFeedsNoResults',
|
key: 'popularFeedsLoading',
|
||||||
type: 'popularFeedsNoResults',
|
type: 'popularFeedsLoading',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
for (const page of popularFeeds.pages || []) {
|
if (!searchResults || searchResults?.length === 0) {
|
||||||
|
slices.push({
|
||||||
|
key: 'popularFeedsNoResults',
|
||||||
|
type: 'popularFeedsNoResults',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
slices = slices.concat(
|
slices = slices.concat(
|
||||||
page.feeds
|
searchResults.map(feed => ({
|
||||||
.filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
|
key: `popularFeed:${feed.uri}`,
|
||||||
.map(feed => ({
|
type: 'popularFeed',
|
||||||
key: `popularFeed:${feed.uri}`,
|
feedUri: 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({
|
slices.push({
|
||||||
key: 'popularFeedsLoadingMore',
|
key: 'popularFeedsNoResults',
|
||||||
type: 'popularFeedsLoadingMore',
|
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
|
return slices
|
||||||
}, [
|
}, [
|
||||||
preferences,
|
preferences,
|
||||||
isPreferencesLoading,
|
isPreferencesLoading,
|
||||||
preferencesError,
|
preferencesError,
|
||||||
popularFeeds,
|
popularFeeds,
|
||||||
isPopularFeedsFetching,
|
isPopularFeedsFetching,
|
||||||
popularFeedsError,
|
popularFeedsError,
|
||||||
isPopularFeedsFetchingNextPage,
|
isPopularFeedsFetchingNextPage,
|
||||||
searchResults,
|
searchResults,
|
||||||
isSearchPending,
|
isSearchPending,
|
||||||
searchError,
|
searchError,
|
||||||
isUserSearching,
|
isUserSearching,
|
||||||
])
|
])
|
||||||
|
|
||||||
const renderHeaderBtn = React.useCallback(() => {
|
const renderHeaderBtn = React.useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href="/settings/saved-feeds"
|
href="/settings/saved-feeds"
|
||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Edit Saved Feeds`)}
|
accessibilityLabel={_(msg`Edit Saved Feeds`)}
|
||||||
accessibilityHint="Opens screen to edit Saved Feeds">
|
accessibilityHint="Opens screen to edit Saved Feeds">
|
||||||
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}, [pal, _])
|
}, [pal, _])
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({item}: {item: FlatlistSlice}) => {
|
({item}: {item: FlatlistSlice}) => {
|
||||||
if (item.type === 'error') {
|
if (item.type === 'error') {
|
||||||
return <ErrorMessage message={item.error} />
|
return <ErrorMessage message={item.error} />
|
||||||
} else if (
|
} else if (
|
||||||
item.type === 'popularFeedsLoadingMore' ||
|
item.type === 'popularFeedsLoadingMore' ||
|
||||||
item.type === 'savedFeedsLoading'
|
item.type === 'savedFeedsLoading'
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<View style={s.p10}>
|
<View style={s.p10}>
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
} else if (item.type === 'savedFeedsHeader') {
|
} else if (item.type === 'savedFeedsHeader') {
|
||||||
if (!isMobile) {
|
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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={{
|
||||||
pal.view,
|
paddingHorizontal: 16,
|
||||||
styles.header,
|
paddingTop: 10,
|
||||||
pal.border,
|
}}>
|
||||||
{
|
<Text type="lg" style={pal.textLight}>
|
||||||
borderBottomWidth: 1,
|
<Trans>You don't have any saved feeds!</Trans>
|
||||||
},
|
</Text>
|
||||||
]}>
|
</View>
|
||||||
<Text type="title-lg" style={[pal.text, s.bold]}>
|
)
|
||||||
<Trans>My Feeds</Trans>
|
} 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>
|
</Text>
|
||||||
<Link
|
|
||||||
href="/settings/saved-feeds"
|
|
||||||
accessibilityLabel={_(msg`Edit My Feeds`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
|
|
||||||
</Link>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <View />
|
return null
|
||||||
} else if (item.type === 'savedFeedNoResults') {
|
},
|
||||||
return (
|
[
|
||||||
<View
|
_,
|
||||||
style={{
|
isMobile,
|
||||||
paddingHorizontal: 16,
|
pal,
|
||||||
paddingTop: 10,
|
query,
|
||||||
}}>
|
onChangeQuery,
|
||||||
<Text type="lg" style={pal.textLight}>
|
onPressCancelSearch,
|
||||||
<Trans>You don't have any saved feeds!</Trans>
|
onSubmitQuery,
|
||||||
</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 && (
|
return (
|
||||||
<SearchInput
|
<View style={[pal.view, styles.container]}>
|
||||||
query={query}
|
{isMobile && (
|
||||||
onChangeQuery={onChangeQuery}
|
<ViewHeader
|
||||||
onPressCancelSearch={onPressCancelSearch}
|
title={_(msg`Feeds`)}
|
||||||
onSubmitQuery={onSubmitQuery}
|
canGoBack={false}
|
||||||
style={{flex: 1, maxWidth: 250}}
|
renderButton={renderHeaderBtn}
|
||||||
/>
|
showBorder
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
[
|
|
||||||
_,
|
|
||||||
isMobile,
|
|
||||||
pal,
|
|
||||||
query,
|
|
||||||
onChangeQuery,
|
|
||||||
onPressCancelSearch,
|
|
||||||
onSubmitQuery,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
{preferences ? <View /> : <ActivityIndicator />}
|
||||||
<View style={[pal.view, styles.container]}>
|
|
||||||
{isMobile && (
|
<FlatList
|
||||||
<ViewHeader
|
style={[!isTabletOrDesktop && s.flex1, styles.list]}
|
||||||
title={_(msg`Feeds`)}
|
data={items}
|
||||||
canGoBack={false}
|
keyExtractor={item => item.key}
|
||||||
renderButton={renderHeaderBtn}
|
contentContainerStyle={styles.contentContainer}
|
||||||
showBorder
|
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 />}
|
<FAB
|
||||||
|
testID="composeFAB"
|
||||||
<FlatList
|
onPress={onPressCompose}
|
||||||
style={[!isTabletOrDesktop && s.flex1, styles.list]}
|
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||||
data={items}
|
accessibilityRole="button"
|
||||||
keyExtractor={item => item.key}
|
accessibilityLabel={_(msg`New post`)}
|
||||||
contentContainerStyle={styles.contentContainer}
|
accessibilityHint=""
|
||||||
renderItem={renderItem}
|
/>
|
||||||
refreshControl={
|
</View>
|
||||||
<RefreshControl
|
)
|
||||||
refreshing={isPTR}
|
},
|
||||||
onRefresh={isUserSearching ? undefined : onPullToRefresh}
|
{isPublic: true},
|
||||||
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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function SavedFeed({feedUri}: {feedUri: string}) {
|
function SavedFeed({feedUri}: {feedUri: string}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
|
@ -14,22 +14,56 @@ import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
|
||||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||||
export const HomeScreen = withAuthRequired(function HomeScreenImpl(
|
export const HomeScreen = withAuthRequired(
|
||||||
props: Props,
|
function HomeScreenImpl(props: Props) {
|
||||||
) {
|
const {hasSession} = useSession()
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
if (preferences) {
|
|
||||||
return <HomeScreenReady {...props} preferences={preferences} />
|
if (!hasSession) {
|
||||||
} else {
|
return <HomeScreenPublic />
|
||||||
return (
|
}
|
||||||
<View style={styles.loading}>
|
|
||||||
<ActivityIndicator size="large" />
|
if (preferences) {
|
||||||
</View>
|
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({
|
function HomeScreenReady({
|
||||||
preferences,
|
preferences,
|
||||||
|
@ -83,6 +117,7 @@ function HomeScreenReady({
|
||||||
emitSoftReset()
|
emitSoftReset()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// TODO(pwi) may need this in public view
|
||||||
const onPageScrollStateChanged = React.useCallback(
|
const onPageScrollStateChanged = React.useCallback(
|
||||||
(state: 'idle' | 'dragging' | 'settling') => {
|
(state: 'idle' | 'dragging' | 'settling') => {
|
||||||
if (state === 'dragging') {
|
if (state === 'dragging') {
|
||||||
|
|
|
@ -11,22 +11,25 @@ import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
|
||||||
export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
|
export const PostLikedByScreen = withAuthRequired(
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
({route}: Props) => {
|
||||||
const {name, rkey} = route.params
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {name, rkey} = route.params
|
||||||
const {_} = useLingui()
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title={_(msg`Liked by`)} />
|
<ViewHeader title={_(msg`Liked by`)} />
|
||||||
<PostLikedByComponent uri={uri} />
|
<PostLikedByComponent uri={uri} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
{isPublic: true},
|
||||||
|
)
|
||||||
|
|
|
@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
|
||||||
export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
|
export const PostRepostedByScreen = withAuthRequired(
|
||||||
const {name, rkey} = route.params
|
({route}: Props) => {
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {name, rkey} = route.params
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
const {_} = useLingui()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title={_(msg`Reposted by`)} />
|
<ViewHeader title={_(msg`Reposted by`)} />
|
||||||
<PostRepostedByComponent uri={uri} />
|
<PostRepostedByComponent uri={uri} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
{isPublic: true},
|
||||||
|
)
|
||||||
|
|
|
@ -27,84 +27,85 @@ import {CenteredView} from '../com/util/Views'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
||||||
export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({
|
export const PostThreadScreen = withAuthRequired(
|
||||||
route,
|
function PostThreadScreenImpl({route}: Props) {
|
||||||
}: Props) {
|
const queryClient = useQueryClient()
|
||||||
const queryClient = useQueryClient()
|
const {_} = useLingui()
|
||||||
const {_} = useLingui()
|
const {fabMinimalShellTransform} = useMinimalShellMode()
|
||||||
const {fabMinimalShellTransform} = useMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const {openComposer} = useComposerControls()
|
||||||
const {openComposer} = useComposerControls()
|
const safeAreaInsets = useSafeAreaInsets()
|
||||||
const safeAreaInsets = useSafeAreaInsets()
|
const {name, rkey} = route.params
|
||||||
const {name, rkey} = route.params
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
|
||||||
const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
|
||||||
|
|
||||||
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 (
|
const onPressReply = React.useCallback(() => {
|
||||||
<View style={s.hContentRegion}>
|
if (!resolvedUri) {
|
||||||
{isMobile && <ViewHeader title={_(msg`Post`)} />}
|
return
|
||||||
<View style={s.flex1}>
|
}
|
||||||
{uriError ? (
|
const thread = queryClient.getQueryData<ThreadNode>(
|
||||||
<CenteredView>
|
POST_THREAD_RQKEY(resolvedUri.uri),
|
||||||
<ErrorMessage message={String(uriError)} />
|
)
|
||||||
</CenteredView>
|
if (thread?.type !== 'post') {
|
||||||
) : (
|
return
|
||||||
<PostThreadComponent
|
}
|
||||||
uri={resolvedUri?.uri}
|
openComposer({
|
||||||
onPressReply={onPressReply}
|
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>
|
</View>
|
||||||
{isMobile && (
|
)
|
||||||
<Animated.View
|
},
|
||||||
style={[
|
{isPublic: true},
|
||||||
styles.prompt,
|
)
|
||||||
fabMinimalShellTransform,
|
|
||||||
{
|
|
||||||
bottom: clamp(safeAreaInsets.bottom, 15, 30),
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<ComposePrompt onPressCompose={onPressReply} />
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
prompt: {
|
prompt: {
|
||||||
|
|
|
@ -43,82 +43,85 @@ interface SectionRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||||
export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
|
export const ProfileScreen = withAuthRequired(
|
||||||
route,
|
function ProfileScreenImpl({route}: Props) {
|
||||||
}: Props) {
|
const {currentAccount} = useSession()
|
||||||
const {currentAccount} = useSession()
|
const name =
|
||||||
const name =
|
route.params.name === 'me' ? currentAccount?.did : route.params.name
|
||||||
route.params.name === 'me' ? currentAccount?.did : route.params.name
|
const moderationOpts = useModerationOpts()
|
||||||
const moderationOpts = useModerationOpts()
|
const {
|
||||||
const {
|
data: resolvedDid,
|
||||||
data: resolvedDid,
|
error: resolveError,
|
||||||
error: resolveError,
|
refetch: refetchDid,
|
||||||
refetch: refetchDid,
|
isFetching: isFetchingDid,
|
||||||
isFetching: isFetchingDid,
|
} = useResolveDidQuery(name)
|
||||||
} = useResolveDidQuery(name)
|
const {
|
||||||
const {
|
data: profile,
|
||||||
data: profile,
|
dataUpdatedAt,
|
||||||
dataUpdatedAt,
|
error: profileError,
|
||||||
error: profileError,
|
refetch: refetchProfile,
|
||||||
refetch: refetchProfile,
|
isFetching: isFetchingProfile,
|
||||||
isFetching: isFetchingProfile,
|
} = useProfileQuery({
|
||||||
} = useProfileQuery({
|
did: resolvedDid?.did,
|
||||||
did: resolvedDid?.did,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const onPressTryAgain = React.useCallback(() => {
|
const onPressTryAgain = React.useCallback(() => {
|
||||||
if (resolveError) {
|
if (resolveError) {
|
||||||
refetchDid()
|
refetchDid()
|
||||||
} else {
|
} else {
|
||||||
refetchProfile()
|
refetchProfile()
|
||||||
|
}
|
||||||
|
}, [resolveError, refetchDid, refetchProfile])
|
||||||
|
|
||||||
|
if (isFetchingDid || isFetchingProfile || !moderationOpts) {
|
||||||
|
return (
|
||||||
|
<CenteredView>
|
||||||
|
<ProfileHeader
|
||||||
|
profile={null}
|
||||||
|
moderation={null}
|
||||||
|
isProfilePreview={true}
|
||||||
|
/>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [resolveError, refetchDid, refetchProfile])
|
if (resolveError || profileError) {
|
||||||
|
return (
|
||||||
if (isFetchingDid || isFetchingProfile || !moderationOpts) {
|
<CenteredView>
|
||||||
return (
|
<ErrorScreen
|
||||||
<CenteredView>
|
testID="profileErrorScreen"
|
||||||
<ProfileHeader
|
title="Oops!"
|
||||||
profile={null}
|
message={cleanError(resolveError || profileError)}
|
||||||
moderation={null}
|
onPressTryAgain={onPressTryAgain}
|
||||||
isProfilePreview={true}
|
/>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (profile && moderationOpts) {
|
||||||
|
return (
|
||||||
|
<ProfileScreenLoaded
|
||||||
|
profile={profile}
|
||||||
|
dataUpdatedAt={dataUpdatedAt}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
hideBackButton={!!route.params.hideBackButton}
|
||||||
/>
|
/>
|
||||||
</CenteredView>
|
)
|
||||||
)
|
}
|
||||||
}
|
// should never happen
|
||||||
if (resolveError || profileError) {
|
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
testID="profileErrorScreen"
|
testID="profileErrorScreen"
|
||||||
title="Oops!"
|
title="Oops!"
|
||||||
message={cleanError(resolveError || profileError)}
|
message="Something went wrong and we're not sure what."
|
||||||
onPressTryAgain={onPressTryAgain}
|
onPressTryAgain={onPressTryAgain}
|
||||||
/>
|
/>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
if (profile && moderationOpts) {
|
{
|
||||||
return (
|
isPublic: true,
|
||||||
<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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function ProfileScreenLoaded({
|
function ProfileScreenLoaded({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
|
|
|
@ -129,6 +129,9 @@ export const ProfileFeedScreen = withAuthRequired(
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
|
||||||
|
|
|
@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'>
|
||||||
export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => {
|
export const ProfileFeedLikedByScreen = withAuthRequired(
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
({route}: Props) => {
|
||||||
const {name, rkey} = route.params
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
|
const {name, rkey} = route.params
|
||||||
const {_} = useLingui()
|
const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title={_(msg`Liked by`)} />
|
<ViewHeader title={_(msg`Liked by`)} />
|
||||||
<PostLikedByComponent uri={uri} />
|
<PostLikedByComponent uri={uri} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
{isPublic: true},
|
||||||
|
)
|
||||||
|
|
|
@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
|
||||||
export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
|
export const ProfileFollowersScreen = withAuthRequired(
|
||||||
const {name} = route.params
|
({route}: Props) => {
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const {name} = route.params
|
||||||
const {_} = useLingui()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title={_(msg`Followers`)} />
|
<ViewHeader title={_(msg`Followers`)} />
|
||||||
<ProfileFollowersComponent name={name} />
|
<ProfileFollowersComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
{isPublic: true},
|
||||||
|
)
|
||||||
|
|
|
@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
|
||||||
export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
|
export const ProfileFollowsScreen = withAuthRequired(
|
||||||
const {name} = route.params
|
({route}: Props) => {
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const {name} = route.params
|
||||||
const {_} = useLingui()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
setMinimalShellMode(false)
|
setMinimalShellMode(false)
|
||||||
}, [setMinimalShellMode]),
|
}, [setMinimalShellMode]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<ViewHeader title={_(msg`Following`)} />
|
<ViewHeader title={_(msg`Following`)} />
|
||||||
<ProfileFollowsComponent name={name} />
|
<ProfileFollowsComponent name={name} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
},
|
||||||
|
{isPublic: true},
|
||||||
|
)
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
usePinFeedMutation,
|
usePinFeedMutation,
|
||||||
useUnpinFeedMutation,
|
useUnpinFeedMutation,
|
||||||
useSetSaveFeedsMutation,
|
useSetSaveFeedsMutation,
|
||||||
usePreferencesQueryKey,
|
preferencesQueryKey,
|
||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
|
|
||||||
|
@ -182,9 +182,10 @@ function ListItem({
|
||||||
const onPressUp = React.useCallback(async () => {
|
const onPressUp = React.useCallback(async () => {
|
||||||
if (!isPinned) return
|
if (!isPinned) return
|
||||||
|
|
||||||
const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
|
const feeds =
|
||||||
usePreferencesQueryKey,
|
queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||||
)?.feeds
|
preferencesQueryKey,
|
||||||
|
)?.feeds
|
||||||
const pinned = feeds?.pinned ?? []
|
const pinned = feeds?.pinned ?? []
|
||||||
const index = pinned.indexOf(feedUri)
|
const index = pinned.indexOf(feedUri)
|
||||||
|
|
||||||
|
@ -206,9 +207,10 @@ function ListItem({
|
||||||
const onPressDown = React.useCallback(async () => {
|
const onPressDown = React.useCallback(async () => {
|
||||||
if (!isPinned) return
|
if (!isPinned) return
|
||||||
|
|
||||||
const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
|
const feeds =
|
||||||
usePreferencesQueryKey,
|
queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||||
)?.feeds
|
preferencesQueryKey,
|
||||||
|
)?.feeds
|
||||||
const pinned = feeds?.pinned ?? []
|
const pinned = feeds?.pinned ?? []
|
||||||
const index = pinned.indexOf(feedUri)
|
const index = pinned.indexOf(feedUri)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue