diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/auth/login/ChooseAccountForm.tsx | 53 | ||||
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 4 | ||||
-rw-r--r-- | src/view/com/feeds/FeedSourceCard.tsx | 11 | ||||
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 31 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 8 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 41 | ||||
-rw-r--r-- | src/view/com/util/LoadingPlaceholder.tsx | 28 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 8 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 22 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/ProfileFeed.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 4 | ||||
-rw-r--r-- | src/view/screens/SavedFeeds.tsx | 133 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 18 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBarWeb.tsx | 2 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 2 |
20 files changed, 247 insertions, 131 deletions
diff --git a/src/view/com/auth/login/ChooseAccountForm.tsx b/src/view/com/auth/login/ChooseAccountForm.tsx index 8c94ef2da..73ddfc9d6 100644 --- a/src/view/com/auth/login/ChooseAccountForm.tsx +++ b/src/view/com/auth/login/ChooseAccountForm.tsx @@ -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" diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 885cd2a15..1a32d29c8 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -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]) diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 64871ca6d..1f2af069b 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -24,6 +24,7 @@ import { useRemoveFeedMutation, } from '#/state/queries/preferences' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' +import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' export function FeedSourceCard({ feedUri, @@ -31,6 +32,7 @@ export function FeedSourceCard({ showSaveBtn = false, showDescription = false, showLikes = false, + LoadingComponent, pinOnSave = false, }: { feedUri: string @@ -38,12 +40,19 @@ export function FeedSourceCard({ 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 diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index ba88f78c0..c496d5f7c 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -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 diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index a39499b24..fdb4df171 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -81,9 +81,10 @@ function FeedsTabBarTablet( ) { const feeds = usePinnedFeedsInfos() const pal = usePalette('default') + const {hasSession} = useSession() const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() - const items = feeds.map(f => f.displayName) + const items = hasSession ? feeds.map(f => f.displayName) : [] return ( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 2983a4575..735aa1bac 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -30,7 +30,7 @@ export function FeedsTabBar( const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) const {headerHeight} = useShellLayout() const {headerMinimalShellTransform} = useMinimalShellMode() - const items = feeds.map(f => f.displayName) + const items = hasSession ? feeds.map(f => f.displayName) : [] const onPressAvi = React.useCallback(() => { setDrawerOpen(true) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index fc6d77696..393c1bc91 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -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 diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 074ab2329..dcbec7cb4 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -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.') + } } } } diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx index 461cbcbe5..74e36ff7b 100644 --- a/src/view/com/util/LoadingPlaceholder.tsx +++ b/src/view/com/util/LoadingPlaceholder.tsx @@ -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> ) } diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 28f01b683..e5a3035a4 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -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} diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 8516d1667..0f442038b 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -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 // = diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 7ddcf17af..89dec5f97 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -403,7 +403,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, () => ({ diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 95589b22a..6d4c0c237 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -501,7 +501,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]) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index cc6d85e6f..9be499561 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -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, () => ({ diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 640d76a5c..858a58a3c 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -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> ) } diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 88cc2d532..579a04b01 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -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, @@ -74,6 +70,8 @@ import {STATUS_PAGE_URL} from 'lib/constants' import {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') @@ -155,13 +153,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}, @@ -190,10 +189,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') diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 9df9b70b3..b2bb6ea1e 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -141,7 +141,7 @@ export function DrawerContent() { } else { if (tab === 'Notifications') { // fetch new notifs on view - queryClient.invalidateQueries({ + queryClient.resetQueries({ queryKey: NOTIFS_RQKEY(), }) } diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index dfb18cc4a..a97ff8afc 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -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(), }) } diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index 8efd7b6b0..3a60bd3b1 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -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> ) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index a0052e0ca..8daa381d5 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -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(), }) } |