Merge remote-tracking branch 'upstream/main' into invite-code-warning
This commit is contained in:
commit
b164f151cc
76 changed files with 1718 additions and 1240 deletions
|
@ -1,23 +1,30 @@
|
|||
import React from 'react'
|
||||
import {ScrollView, TouchableOpacity, View} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {Text} from '../../util/text/Text'
|
||||
import {UserAvatar} from '../../util/UserAvatar'
|
||||
import {s} from 'lib/styles'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {styles} from './styles'
|
||||
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
|
||||
function AccountItem({
|
||||
account,
|
||||
onSelect,
|
||||
isCurrentAccount,
|
||||
}: {
|
||||
account: SessionAccount
|
||||
onSelect: (account: SessionAccount) => void
|
||||
isCurrentAccount: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
@ -48,11 +55,19 @@ function AccountItem({
|
|||
{account.handle}
|
||||
</Text>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
{isCurrentAccount ? (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
size={16}
|
||||
style={[{color: colors.green3} as FontAwesomeIconStyle, s.mr10]}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon="angle-right"
|
||||
size={16}
|
||||
style={[pal.text, s.mr10]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
@ -67,8 +82,9 @@ export const ChooseAccountForm = ({
|
|||
const {track, screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {accounts} = useSession()
|
||||
const {accounts, currentAccount} = useSession()
|
||||
const {initSession} = useSessionApi()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
|
||||
React.useEffect(() => {
|
||||
screen('Choose Account')
|
||||
|
@ -77,13 +93,21 @@ export const ChooseAccountForm = ({
|
|||
const onSelect = React.useCallback(
|
||||
async (account: SessionAccount) => {
|
||||
if (account.accessJwt) {
|
||||
await initSession(account)
|
||||
track('Sign In', {resumedSession: true})
|
||||
if (account.did === currentAccount?.did) {
|
||||
setShowLoggedOut(false)
|
||||
Toast.show(`Already signed in as @${account.handle}`)
|
||||
} else {
|
||||
await initSession(account)
|
||||
track('Sign In', {resumedSession: true})
|
||||
setTimeout(() => {
|
||||
Toast.show(`Signed in as @${account.handle}`)
|
||||
}, 100)
|
||||
}
|
||||
} else {
|
||||
onSelectAccount(account)
|
||||
}
|
||||
},
|
||||
[track, initSession, onSelectAccount],
|
||||
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut],
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -94,7 +118,12 @@ export const ChooseAccountForm = ({
|
|||
<Trans>Sign in as...</Trans>
|
||||
</Text>
|
||||
{accounts.map(account => (
|
||||
<AccountItem key={account.did} account={account} onSelect={onSelect} />
|
||||
<AccountItem
|
||||
key={account.did}
|
||||
account={account}
|
||||
onSelect={onSelect}
|
||||
isCurrentAccount={account.did === currentAccount?.did}
|
||||
/>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
testID="chooseNewAccountBtn"
|
||||
|
|
|
@ -62,7 +62,7 @@ export function FeedPage({
|
|||
const onSoftReset = React.useCallback(() => {
|
||||
if (isPageFocused) {
|
||||
scrollToTop()
|
||||
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
|
||||
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
|
||||
setHasNew(false)
|
||||
}
|
||||
}, [isPageFocused, scrollToTop, queryClient, feed, setHasNew])
|
||||
|
@ -83,7 +83,7 @@ export function FeedPage({
|
|||
|
||||
const onPressLoadLatest = React.useCallback(() => {
|
||||
scrollToTop()
|
||||
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
|
||||
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
|
||||
setHasNew(false)
|
||||
}, [scrollToTop, feed, queryClient, setHasNew])
|
||||
|
||||
|
|
|
@ -17,12 +17,14 @@ import {useModalControls} from '#/state/modals'
|
|||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
usePinFeedMutation,
|
||||
UsePreferencesQueryResponse,
|
||||
usePreferencesQuery,
|
||||
useSaveFeedMutation,
|
||||
useRemoveFeedMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
|
||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
|
||||
export function FeedSourceCard({
|
||||
feedUri,
|
||||
|
@ -30,17 +32,27 @@ export function FeedSourceCard({
|
|||
showSaveBtn = false,
|
||||
showDescription = false,
|
||||
showLikes = false,
|
||||
LoadingComponent,
|
||||
pinOnSave = false,
|
||||
}: {
|
||||
feedUri: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
showSaveBtn?: boolean
|
||||
showDescription?: boolean
|
||||
showLikes?: boolean
|
||||
LoadingComponent?: JSX.Element
|
||||
pinOnSave?: boolean
|
||||
}) {
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
|
||||
|
||||
if (!feed || !preferences) return null
|
||||
if (!feed || !preferences) {
|
||||
return LoadingComponent ? (
|
||||
LoadingComponent
|
||||
) : (
|
||||
<FeedLoadingPlaceholder style={{flex: 1}} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FeedSourceCardLoaded
|
||||
|
@ -50,6 +62,7 @@ export function FeedSourceCard({
|
|||
showSaveBtn={showSaveBtn}
|
||||
showDescription={showDescription}
|
||||
showLikes={showLikes}
|
||||
pinOnSave={pinOnSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -61,6 +74,7 @@ export function FeedSourceCardLoaded({
|
|||
showSaveBtn = false,
|
||||
showDescription = false,
|
||||
showLikes = false,
|
||||
pinOnSave = false,
|
||||
}: {
|
||||
feed: FeedSourceInfo
|
||||
preferences: UsePreferencesQueryResponse
|
||||
|
@ -68,6 +82,7 @@ export function FeedSourceCardLoaded({
|
|||
showSaveBtn?: boolean
|
||||
showDescription?: boolean
|
||||
showLikes?: boolean
|
||||
pinOnSave?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
@ -78,6 +93,7 @@ export function FeedSourceCardLoaded({
|
|||
useSaveFeedMutation()
|
||||
const {isPending: isRemovePending, mutateAsync: removeFeed} =
|
||||
useRemoveFeedMutation()
|
||||
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
|
||||
|
||||
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed.uri))
|
||||
|
||||
|
@ -103,14 +119,18 @@ export function FeedSourceCardLoaded({
|
|||
})
|
||||
} else {
|
||||
try {
|
||||
await saveFeed({uri: feed.uri})
|
||||
if (pinOnSave) {
|
||||
await pinFeed({uri: feed.uri})
|
||||
} else {
|
||||
await saveFeed({uri: feed.uri})
|
||||
}
|
||||
Toast.show('Added to my feeds')
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue contacting your server')
|
||||
logger.error('Failed to save feed', {error: e})
|
||||
}
|
||||
}
|
||||
}, [isSaved, openModal, feed, removeFeed, saveFeed, _])
|
||||
}, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed])
|
||||
|
||||
if (!feed || !preferences) return null
|
||||
|
||||
|
@ -150,7 +170,7 @@ export function FeedSourceCardLoaded({
|
|||
{showSaveBtn && feed.type === 'feed' && (
|
||||
<View>
|
||||
<Pressable
|
||||
disabled={isSavePending || isRemovePending}
|
||||
disabled={isSavePending || isPinPending || isRemovePending}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
isSaved ? 'Remove from my feeds' : 'Add to my feeds'
|
||||
|
|
|
@ -35,15 +35,13 @@ export function Feed({
|
|||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {markAllRead} = useUnreadNotificationsApi()
|
||||
const {markAllRead, checkUnread} = useUnreadNotificationsApi()
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
|
@ -52,13 +50,11 @@ export function Feed({
|
|||
const firstItem = data?.pages[0]?.items[0]
|
||||
|
||||
// mark all read on fresh data
|
||||
// (this will fire each time firstItem changes)
|
||||
React.useEffect(() => {
|
||||
let cleanup
|
||||
if (firstItem) {
|
||||
const to = setTimeout(() => markAllRead(), 250)
|
||||
cleanup = () => clearTimeout(to)
|
||||
markAllRead()
|
||||
}
|
||||
return cleanup
|
||||
}, [firstItem, markAllRead])
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
|
@ -83,7 +79,7 @@ export function Feed({
|
|||
const onRefresh = React.useCallback(async () => {
|
||||
try {
|
||||
setIsPTRing(true)
|
||||
await refetch()
|
||||
await checkUnread({invalidate: true})
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh notifications feed', {
|
||||
error: err,
|
||||
|
@ -91,7 +87,7 @@ export function Feed({
|
|||
} finally {
|
||||
setIsPTRing(false)
|
||||
}
|
||||
}, [refetch, setIsPTRing])
|
||||
}, [checkUnread, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
@ -136,21 +132,6 @@ export function Feed({
|
|||
[onPressRetryLoadMore, moderationOpts],
|
||||
)
|
||||
|
||||
const showHeaderSpinner = !isPTRing && isFetching && !isLoading
|
||||
const FeedHeader = React.useCallback(
|
||||
() => (
|
||||
<View>
|
||||
{ListHeaderComponent ? <ListHeaderComponent /> : null}
|
||||
{showHeaderSpinner ? (
|
||||
<View style={{padding: 10}}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
),
|
||||
[ListHeaderComponent, showHeaderSpinner],
|
||||
)
|
||||
|
||||
const FeedFooter = React.useCallback(
|
||||
() =>
|
||||
isFetchingNextPage ? (
|
||||
|
@ -180,7 +161,7 @@ export function Feed({
|
|||
data={items}
|
||||
keyExtractor={item => item._reactKey}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={FeedHeader}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListFooterComponent={FeedFooter}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
|
|
|
@ -12,6 +12,9 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
|
|||
import {useSession} from '#/state/session'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
export function FeedsTabBar(
|
||||
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||
|
@ -79,11 +82,37 @@ function FeedsTabBarPublic() {
|
|||
function FeedsTabBarTablet(
|
||||
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||
) {
|
||||
const feeds = usePinnedFeedsInfos()
|
||||
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
|
||||
const pal = usePalette('default')
|
||||
const {hasSession} = useSession()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
const {headerHeight} = useShellLayout()
|
||||
const items = feeds.map(f => f.displayName)
|
||||
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
|
||||
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
|
||||
const items = showFeedsLinkInTabBar
|
||||
? pinnedDisplayNames.concat('Feeds ✨')
|
||||
: pinnedDisplayNames
|
||||
|
||||
const onPressDiscoverFeeds = React.useCallback(() => {
|
||||
if (isWeb) {
|
||||
navigation.navigate('Feeds')
|
||||
} else {
|
||||
navigation.navigate('FeedsTab')
|
||||
navigation.popToTop()
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
if (showFeedsLinkInTabBar && index === items.length - 1) {
|
||||
onPressDiscoverFeeds()
|
||||
} else if (props.onSelect) {
|
||||
props.onSelect(index)
|
||||
}
|
||||
},
|
||||
[items.length, onPressDiscoverFeeds, props, showFeedsLinkInTabBar],
|
||||
)
|
||||
|
||||
return (
|
||||
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
|
||||
|
@ -95,6 +124,7 @@ function FeedsTabBarTablet(
|
|||
<TabBar
|
||||
key={items.join(',')}
|
||||
{...props}
|
||||
onSelect={onSelect}
|
||||
items={items}
|
||||
indicatorColor={pal.colors.link}
|
||||
/>
|
||||
|
|
|
@ -18,6 +18,9 @@ import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
|||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {useSession} from '#/state/session'
|
||||
import {usePinnedFeedsInfos} from '#/state/queries/feed'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
export function FeedsTabBar(
|
||||
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||
|
@ -26,11 +29,36 @@ export function FeedsTabBar(
|
|||
const {isSandbox, hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
const setDrawerOpen = useSetDrawerOpen()
|
||||
const feeds = usePinnedFeedsInfos()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
|
||||
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
|
||||
const {headerHeight} = useShellLayout()
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
const items = feeds.map(f => f.displayName)
|
||||
const pinnedDisplayNames = hasSession ? feeds.map(f => f.displayName) : []
|
||||
const showFeedsLinkInTabBar = hasSession && !hasPinnedCustom
|
||||
const items = showFeedsLinkInTabBar
|
||||
? pinnedDisplayNames.concat('Feeds ✨')
|
||||
: pinnedDisplayNames
|
||||
|
||||
const onPressFeedsLink = React.useCallback(() => {
|
||||
if (isWeb) {
|
||||
navigation.navigate('Feeds')
|
||||
} else {
|
||||
navigation.navigate('FeedsTab')
|
||||
navigation.popToTop()
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const onSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
if (showFeedsLinkInTabBar && index === items.length - 1) {
|
||||
onPressFeedsLink()
|
||||
} else if (props.onSelect) {
|
||||
props.onSelect(index)
|
||||
}
|
||||
},
|
||||
[items.length, onPressFeedsLink, props, showFeedsLinkInTabBar],
|
||||
)
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
|
@ -84,7 +112,7 @@ export function FeedsTabBar(
|
|||
key={items.join(',')}
|
||||
onPressSelected={props.onPressSelected}
|
||||
selectedPage={props.selectedPage}
|
||||
onSelect={props.onSelect}
|
||||
onSelect={onSelect}
|
||||
testID={props.testID}
|
||||
items={items}
|
||||
indicatorColor={pal.colors.link}
|
||||
|
|
|
@ -108,6 +108,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
pointerEvents: isHeaderReady ? 'auto' : 'none',
|
||||
}}>
|
||||
<TabBar
|
||||
testID={testID}
|
||||
items={items}
|
||||
selectedPage={currentPage}
|
||||
onSelect={props.onSelect}
|
||||
|
@ -127,6 +128,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||
isMobile,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
testID,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ export function TabBar({
|
|||
return (
|
||||
<View testID={testID} style={[pal.view, styles.outer]}>
|
||||
<DraggableScrollView
|
||||
testID={`${testID}-selector`}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
ref={scrollElRef}
|
||||
|
@ -76,6 +77,7 @@ export function TabBar({
|
|||
const selected = i === selectedPage
|
||||
return (
|
||||
<PressableWithHover
|
||||
testID={`${testID}-selector-${i}`}
|
||||
key={item}
|
||||
onLayout={e => onItemLayout(e, i)}
|
||||
style={[styles.item, selected && indicatorStyle]}
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
FeedDescriptor,
|
||||
FeedParams,
|
||||
usePostFeedQuery,
|
||||
pollLatest,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
|
||||
|
@ -84,22 +85,21 @@ let Feed = ({
|
|||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
fetchNextPage,
|
||||
pollLatest,
|
||||
} = usePostFeedQuery(feed, feedParams, opts)
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
|
||||
|
||||
const checkForNew = React.useCallback(async () => {
|
||||
if (!isFetched || isFetching || !onHasNew) {
|
||||
if (!data?.pages[0] || isFetching || !onHasNew) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (await pollLatest()) {
|
||||
if (await pollLatest(data.pages[0])) {
|
||||
onHasNew(true)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Poll latest failed', {feed, error: String(e)})
|
||||
}
|
||||
}, [feed, isFetched, isFetching, pollLatest, onHasNew])
|
||||
}, [feed, data, isFetching, onHasNew])
|
||||
|
||||
React.useEffect(() => {
|
||||
// we store the interval handler in a ref to avoid needless
|
||||
|
|
|
@ -17,28 +17,15 @@ import {EmptyState} from '../util/EmptyState'
|
|||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useRemoveFeedMutation} from '#/state/queries/preferences'
|
||||
|
||||
enum KnownError {
|
||||
Block,
|
||||
FeedgenDoesNotExist,
|
||||
FeedgenMisconfigured,
|
||||
FeedgenBadResponse,
|
||||
FeedgenOffline,
|
||||
FeedgenUnknown,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
const MESSAGES = {
|
||||
[KnownError.Unknown]: '',
|
||||
[KnownError.Block]: '',
|
||||
[KnownError.FeedgenDoesNotExist]: `Hmmm, we're having trouble finding this feed. It may have been deleted.`,
|
||||
[KnownError.FeedgenMisconfigured]:
|
||||
'Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.',
|
||||
[KnownError.FeedgenBadResponse]:
|
||||
'Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.',
|
||||
[KnownError.FeedgenOffline]:
|
||||
'Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.',
|
||||
[KnownError.FeedgenUnknown]:
|
||||
'Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.',
|
||||
export enum KnownError {
|
||||
Block = 'Block',
|
||||
FeedgenDoesNotExist = 'FeedgenDoesNotExist',
|
||||
FeedgenMisconfigured = 'FeedgenMisconfigured',
|
||||
FeedgenBadResponse = 'FeedgenBadResponse',
|
||||
FeedgenOffline = 'FeedgenOffline',
|
||||
FeedgenUnknown = 'FeedgenUnknown',
|
||||
FeedNSFPublic = 'FeedNSFPublic',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
||||
export function FeedErrorMessage({
|
||||
|
@ -90,7 +77,32 @@ function FeedgenErrorMessage({
|
|||
const pal = usePalette('default')
|
||||
const {_: _l} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const msg = MESSAGES[knownError]
|
||||
const msg = React.useMemo(
|
||||
() =>
|
||||
({
|
||||
[KnownError.Unknown]: '',
|
||||
[KnownError.Block]: '',
|
||||
[KnownError.FeedgenDoesNotExist]: _l(
|
||||
msgLingui`Hmmm, we're having trouble finding this feed. It may have been deleted.`,
|
||||
),
|
||||
[KnownError.FeedgenMisconfigured]: _l(
|
||||
msgLingui`Hmm, the feed server appears to be misconfigured. Please let the feed owner know about this issue.`,
|
||||
),
|
||||
[KnownError.FeedgenBadResponse]: _l(
|
||||
msgLingui`Hmm, the feed server gave a bad response. Please let the feed owner know about this issue.`,
|
||||
),
|
||||
[KnownError.FeedgenOffline]: _l(
|
||||
msgLingui`Hmm, the feed server appears to be offline. Please let the feed owner know about this issue.`,
|
||||
),
|
||||
[KnownError.FeedNSFPublic]: _l(
|
||||
msgLingui`We're sorry, but this content is not viewable without a Bluesky account.`,
|
||||
),
|
||||
[KnownError.FeedgenUnknown]: _l(
|
||||
msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`,
|
||||
),
|
||||
}[knownError]),
|
||||
[_l, knownError],
|
||||
)
|
||||
const [_, uri] = feedDesc.split('|')
|
||||
const [ownerDid] = safeParseFeedgenUri(uri)
|
||||
const {openModal, closeModal} = useModalControls()
|
||||
|
@ -121,6 +133,36 @@ function FeedgenErrorMessage({
|
|||
})
|
||||
}, [openModal, closeModal, uri, removeFeed, _l])
|
||||
|
||||
const cta = React.useMemo(() => {
|
||||
switch (knownError) {
|
||||
case KnownError.FeedNSFPublic: {
|
||||
return null
|
||||
}
|
||||
case KnownError.FeedgenDoesNotExist:
|
||||
case KnownError.FeedgenMisconfigured:
|
||||
case KnownError.FeedgenBadResponse:
|
||||
case KnownError.FeedgenOffline:
|
||||
case KnownError.FeedgenUnknown: {
|
||||
return (
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
||||
{knownError === KnownError.FeedgenDoesNotExist && (
|
||||
<Button
|
||||
type="inverted"
|
||||
label="Remove feed"
|
||||
onPress={onRemoveFeed}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="default-light"
|
||||
label="View profile"
|
||||
onPress={onViewProfile}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [knownError, onViewProfile, onRemoveFeed])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
|
@ -134,16 +176,7 @@ function FeedgenErrorMessage({
|
|||
},
|
||||
]}>
|
||||
<Text style={pal.text}>{msg}</Text>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
||||
{knownError === KnownError.FeedgenDoesNotExist && (
|
||||
<Button type="inverted" label="Remove feed" onPress={onRemoveFeed} />
|
||||
)}
|
||||
<Button
|
||||
type="default-light"
|
||||
label="View profile"
|
||||
onPress={onViewProfile}
|
||||
/>
|
||||
</View>
|
||||
{cta}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
@ -196,5 +229,8 @@ function detectKnownError(
|
|||
if (error.includes('feed provided an invalid response')) {
|
||||
return KnownError.FeedgenBadResponse
|
||||
}
|
||||
if (error.includes(KnownError.FeedNSFPublic)) {
|
||||
return KnownError.FeedNSFPublic
|
||||
}
|
||||
return KnownError.FeedgenUnknown
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> {
|
|||
noFeedback?: boolean
|
||||
asAnchor?: boolean
|
||||
anchorNoUnderline?: boolean
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
}
|
||||
|
||||
export const Link = memo(function Link({
|
||||
|
@ -58,6 +59,7 @@ export const Link = memo(function Link({
|
|||
asAnchor,
|
||||
accessible,
|
||||
anchorNoUnderline,
|
||||
navigationAction,
|
||||
...props
|
||||
}: Props) {
|
||||
const {closeModal} = useModalControls()
|
||||
|
@ -67,10 +69,16 @@ export const Link = memo(function Link({
|
|||
const onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
if (typeof href === 'string') {
|
||||
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
navigation,
|
||||
sanitizeUrl(href),
|
||||
navigationAction,
|
||||
e,
|
||||
)
|
||||
}
|
||||
},
|
||||
[closeModal, navigation, href],
|
||||
[closeModal, navigation, navigationAction, href],
|
||||
)
|
||||
|
||||
if (noFeedback) {
|
||||
|
@ -146,6 +154,7 @@ export const TextLink = memo(function TextLink({
|
|||
title,
|
||||
onPress,
|
||||
warnOnMismatchingLabel,
|
||||
navigationAction,
|
||||
...orgProps
|
||||
}: {
|
||||
testID?: string
|
||||
|
@ -158,6 +167,7 @@ export const TextLink = memo(function TextLink({
|
|||
dataSet?: any
|
||||
title?: string
|
||||
warnOnMismatchingLabel?: boolean
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
} & TextProps) {
|
||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
@ -185,7 +195,13 @@ export const TextLink = memo(function TextLink({
|
|||
// @ts-ignore function signature differs by platform -prf
|
||||
return onPress()
|
||||
}
|
||||
return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
|
||||
return onPressInner(
|
||||
closeModal,
|
||||
navigation,
|
||||
sanitizeUrl(href),
|
||||
navigationAction,
|
||||
e,
|
||||
)
|
||||
},
|
||||
[
|
||||
onPress,
|
||||
|
@ -195,6 +211,7 @@ export const TextLink = memo(function TextLink({
|
|||
href,
|
||||
text,
|
||||
warnOnMismatchingLabel,
|
||||
navigationAction,
|
||||
],
|
||||
)
|
||||
const hrefAttrs = useMemo(() => {
|
||||
|
@ -241,6 +258,7 @@ interface TextLinkOnWebOnlyProps extends TextProps {
|
|||
accessibilityLabel?: string
|
||||
accessibilityHint?: string
|
||||
title?: string
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
}
|
||||
export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
||||
testID,
|
||||
|
@ -250,6 +268,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
|||
text,
|
||||
numberOfLines,
|
||||
lineHeight,
|
||||
navigationAction,
|
||||
...props
|
||||
}: TextLinkOnWebOnlyProps) {
|
||||
if (isWeb) {
|
||||
|
@ -263,6 +282,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
|
|||
numberOfLines={numberOfLines}
|
||||
lineHeight={lineHeight}
|
||||
title={props.title}
|
||||
navigationAction={navigationAction}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -296,6 +316,7 @@ function onPressInner(
|
|||
closeModal = () => {},
|
||||
navigation: NavigationProp,
|
||||
href: string,
|
||||
navigationAction: 'push' | 'replace' | 'navigate' = 'push',
|
||||
e?: Event,
|
||||
) {
|
||||
let shouldHandle = false
|
||||
|
@ -328,8 +349,18 @@ function onPressInner(
|
|||
} else {
|
||||
closeModal() // close any active modals
|
||||
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
if (navigationAction === 'push') {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.dispatch(StackActions.push(...router.matchPath(href)))
|
||||
} else if (navigationAction === 'replace') {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.dispatch(StackActions.replace(...router.matchPath(href)))
|
||||
} else if (navigationAction === 'navigate') {
|
||||
// @ts-ignore we're not able to type check on this one -prf
|
||||
navigation.navigate(...router.matchPath(href))
|
||||
} else {
|
||||
throw Error('Unsupported navigator action.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -171,14 +171,22 @@ export function ProfileCardFeedLoadingPlaceholder() {
|
|||
|
||||
export function FeedLoadingPlaceholder({
|
||||
style,
|
||||
showLowerPlaceholder = true,
|
||||
showTopBorder = true,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
showTopBorder?: boolean
|
||||
showLowerPlaceholder?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{paddingHorizontal: 12, paddingVertical: 18, borderTopWidth: 1},
|
||||
{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 18,
|
||||
borderTopWidth: showTopBorder ? 1 : 0,
|
||||
},
|
||||
pal.border,
|
||||
style,
|
||||
]}>
|
||||
|
@ -193,14 +201,16 @@ export function FeedLoadingPlaceholder({
|
|||
<LoadingPlaceholder width={120} height={8} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{paddingHorizontal: 5}}>
|
||||
<LoadingPlaceholder
|
||||
width={260}
|
||||
height={8}
|
||||
style={{marginVertical: 12}}
|
||||
/>
|
||||
<LoadingPlaceholder width={120} height={8} />
|
||||
</View>
|
||||
{showLowerPlaceholder && (
|
||||
<View style={{paddingHorizontal: 5}}>
|
||||
<LoadingPlaceholder
|
||||
width={260}
|
||||
height={8}
|
||||
style={{marginVertical: 12}}
|
||||
/>
|
||||
<LoadingPlaceholder width={120} height={8} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -437,6 +437,7 @@ export function FeedsScreen(_props: Props) {
|
|||
showSaveBtn={hasSession}
|
||||
showDescription
|
||||
showLikes
|
||||
pinOnSave
|
||||
/>
|
||||
)
|
||||
} else if (item.type === 'popularFeedsNoResults') {
|
||||
|
|
|
@ -40,6 +40,12 @@ function HomeScreenReady({
|
|||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||
|
||||
/**
|
||||
* Used to ensure that we re-compute `customFeeds` AND force a re-render of
|
||||
* the pager with the new order of feeds.
|
||||
*/
|
||||
const pinnedFeedOrderKey = JSON.stringify(preferences.feeds.pinned)
|
||||
|
||||
const customFeeds = React.useMemo(() => {
|
||||
const pinned = preferences.feeds.pinned
|
||||
const feeds: FeedDescriptor[] = []
|
||||
|
@ -83,7 +89,6 @@ function HomeScreenReady({
|
|||
emitSoftReset()
|
||||
}, [])
|
||||
|
||||
// TODO(pwi) may need this in public view
|
||||
const onPageScrollStateChanged = React.useCallback(
|
||||
(state: 'idle' | 'dragging' | 'settling') => {
|
||||
if (state === 'dragging') {
|
||||
|
@ -118,6 +123,7 @@ function HomeScreenReady({
|
|||
|
||||
return hasSession ? (
|
||||
<Pager
|
||||
key={pinnedFeedOrderKey}
|
||||
testID="homeScreen"
|
||||
onPageSelected={onPageSelected}
|
||||
onPageScrollStateChanged={onPageScrollStateChanged}
|
||||
|
|
|
@ -19,7 +19,10 @@ import {logger} from '#/logger'
|
|||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
import {
|
||||
useUnreadNotifications,
|
||||
useUnreadNotificationsApi,
|
||||
} from '#/state/queries/notifications/unread'
|
||||
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
|
||||
import {listenSoftReset, emitSoftReset} from '#/state/events'
|
||||
|
||||
|
@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) {
|
|||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
const queryClient = useQueryClient()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
const unreadApi = useUnreadNotificationsApi()
|
||||
const hasNew = !!unreadNotifs
|
||||
|
||||
// event handlers
|
||||
|
@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) {
|
|||
|
||||
const onPressLoadLatest = React.useCallback(() => {
|
||||
scrollToTop()
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}, [scrollToTop, queryClient])
|
||||
if (hasNew) {
|
||||
// render what we have now
|
||||
queryClient.resetQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
} else {
|
||||
// check with the server
|
||||
unreadApi.checkUnread({invalidate: true})
|
||||
}
|
||||
}, [scrollToTop, queryClient, unreadApi, hasNew])
|
||||
|
||||
// on-visible setup
|
||||
// =
|
||||
|
|
|
@ -267,6 +267,7 @@ function ProfileScreenLoaded({
|
|||
screenDescription="profile"
|
||||
moderation={moderation.account}>
|
||||
<PagerWithHeader
|
||||
testID="profilePager"
|
||||
isHeaderReady={true}
|
||||
items={sectionTitles}
|
||||
onPageSelected={onPageSelected}
|
||||
|
@ -403,7 +404,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
|||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
|
||||
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
|
|
|
@ -353,6 +353,7 @@ export function ProfileFeedScreenInner({
|
|||
style={styles.btn}
|
||||
/>
|
||||
<Button
|
||||
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
|
||||
disabled={isPinPending || isUnpinPending}
|
||||
type={isPinned ? 'default' : 'inverted'}
|
||||
label={isPinned ? 'Unpin' : 'Pin to home'}
|
||||
|
@ -501,7 +502,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
|||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
|
||||
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ function ProfileListScreenLoaded({
|
|||
list,
|
||||
onChange() {
|
||||
if (isCurateList) {
|
||||
queryClient.invalidateQueries({
|
||||
queryClient.resetQueries({
|
||||
// TODO(eric) should construct these strings with a fn too
|
||||
queryKey: FEED_RQKEY(`list|${list.uri}`),
|
||||
})
|
||||
|
@ -530,7 +530,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
|||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
|
||||
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
|
||||
queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
|
||||
setHasNew(false)
|
||||
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
} from 'react-native'
|
||||
import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
@ -32,9 +25,8 @@ import {
|
|||
usePinFeedMutation,
|
||||
useUnpinFeedMutation,
|
||||
useSetSaveFeedsMutation,
|
||||
preferencesQueryKey,
|
||||
UsePreferencesQueryResponse,
|
||||
} from '#/state/queries/preferences'
|
||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
|
||||
const HITSLOP_TOP = {
|
||||
top: 20,
|
||||
|
@ -57,6 +49,24 @@ export function SavedFeeds({}: Props) {
|
|||
const {screen} = useAnalytics()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const {
|
||||
mutateAsync: setSavedFeeds,
|
||||
variables: optimisticSavedFeedsResponse,
|
||||
reset: resetSaveFeedsMutationState,
|
||||
error: setSavedFeedsError,
|
||||
} = useSetSaveFeedsMutation()
|
||||
|
||||
/*
|
||||
* Use optimistic data if exists and no error, otherwise fallback to remote
|
||||
* data
|
||||
*/
|
||||
const currentFeeds =
|
||||
optimisticSavedFeedsResponse && !setSavedFeedsError
|
||||
? optimisticSavedFeedsResponse
|
||||
: preferences?.feeds || {saved: [], pinned: []}
|
||||
const unpinned = currentFeeds.saved.filter(f => {
|
||||
return !currentFeeds.pinned?.includes(f)
|
||||
})
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
|
@ -80,7 +90,7 @@ export function SavedFeeds({}: Props) {
|
|||
</Text>
|
||||
</View>
|
||||
{preferences?.feeds ? (
|
||||
!preferences.feeds.pinned.length ? (
|
||||
!currentFeeds.pinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
|
@ -93,8 +103,15 @@ export function SavedFeeds({}: Props) {
|
|||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
preferences?.feeds?.pinned?.map(uri => (
|
||||
<ListItem key={uri} feedUri={uri} isPinned />
|
||||
currentFeeds.pinned.map(uri => (
|
||||
<ListItem
|
||||
key={uri}
|
||||
feedUri={uri}
|
||||
isPinned
|
||||
setSavedFeeds={setSavedFeeds}
|
||||
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
||||
currentFeeds={currentFeeds}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
|
@ -106,7 +123,7 @@ export function SavedFeeds({}: Props) {
|
|||
</Text>
|
||||
</View>
|
||||
{preferences?.feeds ? (
|
||||
!preferences.feeds.unpinned.length ? (
|
||||
!unpinned.length ? (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
|
@ -119,8 +136,15 @@ export function SavedFeeds({}: Props) {
|
|||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
preferences.feeds.unpinned.map(uri => (
|
||||
<ListItem key={uri} feedUri={uri} isPinned={false} />
|
||||
unpinned.map(uri => (
|
||||
<ListItem
|
||||
key={uri}
|
||||
feedUri={uri}
|
||||
isPinned={false}
|
||||
setSavedFeeds={setSavedFeeds}
|
||||
resetSaveFeedsMutationState={resetSaveFeedsMutationState}
|
||||
currentFeeds={currentFeeds}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
|
@ -151,22 +175,30 @@ export function SavedFeeds({}: Props) {
|
|||
function ListItem({
|
||||
feedUri,
|
||||
isPinned,
|
||||
currentFeeds,
|
||||
setSavedFeeds,
|
||||
resetSaveFeedsMutationState,
|
||||
}: {
|
||||
feedUri: string // uri
|
||||
isPinned: boolean
|
||||
currentFeeds: {saved: string[]; pinned: string[]}
|
||||
setSavedFeeds: ReturnType<typeof useSetSaveFeedsMutation>['mutateAsync']
|
||||
resetSaveFeedsMutationState: ReturnType<
|
||||
typeof useSetSaveFeedsMutation
|
||||
>['reset']
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const queryClient = useQueryClient()
|
||||
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
|
||||
const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
|
||||
useUnpinFeedMutation()
|
||||
const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
|
||||
useSetSaveFeedsMutation()
|
||||
const isPending = isPinPending || isUnpinPending
|
||||
|
||||
const onTogglePinned = React.useCallback(async () => {
|
||||
Haptics.default()
|
||||
|
||||
try {
|
||||
resetSaveFeedsMutationState()
|
||||
|
||||
if (isPinned) {
|
||||
await unpinFeed({uri: feedUri})
|
||||
} else {
|
||||
|
@ -176,23 +208,20 @@ function ListItem({
|
|||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to toggle pinned feed', {error: e})
|
||||
}
|
||||
}, [feedUri, isPinned, pinFeed, unpinFeed])
|
||||
}, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
|
||||
|
||||
const onPressUp = React.useCallback(async () => {
|
||||
if (!isPinned) return
|
||||
|
||||
const feeds =
|
||||
queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||
preferencesQueryKey,
|
||||
)?.feeds
|
||||
const pinned = feeds?.pinned ?? []
|
||||
// create new array, do not mutate
|
||||
const pinned = [...currentFeeds.pinned]
|
||||
const index = pinned.indexOf(feedUri)
|
||||
|
||||
if (index === -1 || index === 0) return
|
||||
;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
|
||||
|
||||
try {
|
||||
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
|
||||
await setSavedFeeds({saved: currentFeeds.saved, pinned})
|
||||
track('CustomFeed:Reorder', {
|
||||
uri: feedUri,
|
||||
index: pinned.indexOf(feedUri),
|
||||
|
@ -201,23 +230,19 @@ function ListItem({
|
|||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}
|
||||
}, [feedUri, isPinned, queryClient, setSavedFeeds])
|
||||
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
|
||||
|
||||
const onPressDown = React.useCallback(async () => {
|
||||
if (!isPinned) return
|
||||
|
||||
const feeds =
|
||||
queryClient.getQueryData<UsePreferencesQueryResponse>(
|
||||
preferencesQueryKey,
|
||||
)?.feeds
|
||||
const pinned = feeds?.pinned ?? []
|
||||
const pinned = [...currentFeeds.pinned]
|
||||
const index = pinned.indexOf(feedUri)
|
||||
|
||||
if (index === -1 || index >= pinned.length - 1) return
|
||||
;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
|
||||
|
||||
try {
|
||||
await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
|
||||
await setSavedFeeds({saved: currentFeeds.saved, pinned})
|
||||
track('CustomFeed:Reorder', {
|
||||
uri: feedUri,
|
||||
index: pinned.indexOf(feedUri),
|
||||
|
@ -226,7 +251,7 @@ function ListItem({
|
|||
Toast.show('There was an issue contacting the server')
|
||||
logger.error('Failed to set pinned feed order', {error: e})
|
||||
}
|
||||
}, [feedUri, isPinned, queryClient, setSavedFeeds])
|
||||
}, [feedUri, isPinned, setSavedFeeds, currentFeeds])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
@ -234,24 +259,30 @@ function ListItem({
|
|||
style={[styles.itemContainer, pal.border]}>
|
||||
{isPinned ? (
|
||||
<View style={styles.webArrowButtonsContainer}>
|
||||
<TouchableOpacity
|
||||
disabled={isMovePending}
|
||||
<Pressable
|
||||
disabled={isPending}
|
||||
accessibilityRole="button"
|
||||
onPress={onPressUp}
|
||||
hitSlop={HITSLOP_TOP}>
|
||||
hitSlop={HITSLOP_TOP}
|
||||
style={state => ({
|
||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
||||
})}>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-up"
|
||||
size={12}
|
||||
style={[pal.text, styles.webArrowUpButton]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
disabled={isMovePending}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
disabled={isPending}
|
||||
accessibilityRole="button"
|
||||
onPress={onPressDown}
|
||||
hitSlop={HITSLOP_BOTTOM}>
|
||||
hitSlop={HITSLOP_BOTTOM}
|
||||
style={state => ({
|
||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
||||
})}>
|
||||
<FontAwesomeIcon icon="arrow-down" size={12} style={[pal.text]} />
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
<FeedSourceCard
|
||||
|
@ -259,18 +290,28 @@ function ListItem({
|
|||
feedUri={feedUri}
|
||||
style={styles.noBorder}
|
||||
showSaveBtn
|
||||
LoadingComponent={
|
||||
<FeedLoadingPlaceholder
|
||||
style={{flex: 1}}
|
||||
showLowerPlaceholder={false}
|
||||
showTopBorder={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
disabled={isPinPending || isUnpinPending}
|
||||
<Pressable
|
||||
disabled={isPending}
|
||||
accessibilityRole="button"
|
||||
hitSlop={10}
|
||||
onPress={onTogglePinned}>
|
||||
onPress={onTogglePinned}
|
||||
style={state => ({
|
||||
opacity: state.hovered || state.focused || isPending ? 0.5 : 1,
|
||||
})}>
|
||||
<FontAwesomeIcon
|
||||
icon="thumb-tack"
|
||||
size={20}
|
||||
color={isPinned ? colors.blue3 : pal.colors.icon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,11 +10,7 @@ import {
|
|||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
|
@ -73,6 +69,8 @@ import {STATUS_PAGE_URL} from 'lib/constants'
|
|||
import {Plural, Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
|
||||
function SettingsAccountCard({account}: {account: SessionAccount}) {
|
||||
const pal = usePalette('default')
|
||||
|
@ -154,13 +152,14 @@ export function SettingsScreen({}: Props) {
|
|||
const {screen, track} = useAnalytics()
|
||||
const {openModal} = useModalControls()
|
||||
const {isSwitchingAccounts, accounts, currentAccount} = useSession()
|
||||
const {clearCurrentAccount} = useSessionApi()
|
||||
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
|
||||
getAgent(),
|
||||
)
|
||||
const {mutate: clearPreferences} = useClearPreferencesMutation()
|
||||
const {data: invites} = useInviteCodesQuery()
|
||||
const invitesAvailable = invites?.available?.length ?? 0
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAllActiveElements = useCloseAllActiveElements()
|
||||
|
||||
const primaryBg = useCustomPalette<ViewStyle>({
|
||||
light: {backgroundColor: colors.blue0},
|
||||
|
@ -189,10 +188,9 @@ export function SettingsScreen({}: Props) {
|
|||
|
||||
const onPressAddAccount = React.useCallback(() => {
|
||||
track('Settings:AddAccountButtonClicked')
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
clearCurrentAccount()
|
||||
}, [track, navigation, clearCurrentAccount])
|
||||
setShowLoggedOut(true)
|
||||
closeAllActiveElements()
|
||||
}, [track, setShowLoggedOut, closeAllActiveElements])
|
||||
|
||||
const onPressChangeHandle = React.useCallback(() => {
|
||||
track('Settings:ChangeHandleButtonClicked')
|
||||
|
|
|
@ -141,7 +141,7 @@ export function DrawerContent() {
|
|||
} else {
|
||||
if (tab === 'Notifications') {
|
||||
// fetch new notifs on view
|
||||
queryClient.invalidateQueries({
|
||||
queryClient.resetQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
|
|||
} else {
|
||||
if (tab === 'Notifications') {
|
||||
// fetch new notifs on view
|
||||
queryClient.invalidateQueries({
|
||||
queryClient.resetQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ const NavItem: React.FC<{
|
|||
: isTab(currentRoute.name, routeName)
|
||||
|
||||
return (
|
||||
<Link href={href} style={styles.ctrl}>
|
||||
<Link href={href} style={styles.ctrl} navigationAction="navigate">
|
||||
{children({isActive})}
|
||||
</Link>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ import {usePinnedFeedsInfos} from '#/state/queries/feed'
|
|||
export function DesktopFeeds() {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const feeds = usePinnedFeedsInfos()
|
||||
const {feeds} = usePinnedFeedsInfos()
|
||||
|
||||
const route = useNavigationState(state => {
|
||||
if (!state) {
|
||||
|
|
|
@ -150,7 +150,7 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
|
|||
} else {
|
||||
if (href === '/notifications') {
|
||||
// fetch new notifs on view
|
||||
queryClient.invalidateQueries({
|
||||
queryClient.resetQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue