diff options
Diffstat (limited to 'src/view/com')
-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 | 28 | ||||
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 31 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBar.web.tsx | 34 | ||||
-rw-r--r-- | src/view/com/pager/FeedsTabBarMobile.tsx | 34 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 2 | ||||
-rw-r--r-- | src/view/com/pager/TabBar.tsx | 2 | ||||
-rw-r--r-- | src/view/com/posts/Feed.tsx | 8 | ||||
-rw-r--r-- | src/view/com/posts/FeedErrorMessage.tsx | 102 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 41 | ||||
-rw-r--r-- | src/view/com/util/LoadingPlaceholder.tsx | 28 |
12 files changed, 268 insertions, 99 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 d8b67767b..1f2af069b 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -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' 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..57c83f17c 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -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} /> diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index 2983a4575..882b6cfc5 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -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} diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 3d2a3c55f..2c7640c43 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -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, ], ) diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 0e08b22d8..c3a95c5c0 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -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]} 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/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index 5a9290f66..63d9d5956 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -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 } 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> ) } |