diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/feeds/FeedPage.tsx | 26 | ||||
-rw-r--r-- | src/view/com/posts/PostFeed.tsx | 83 | ||||
-rw-r--r-- | src/view/com/util/load-latest/LoadLatestBtn.tsx | 105 |
3 files changed, 103 insertions, 111 deletions
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 604533b0f..e8a177a8d 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api' import {msg} from '@lingui/macro' @@ -58,14 +58,14 @@ export function FeedPage({ const navigation = useNavigation<NavigationProp<AllNavigatorParams>>() const queryClient = useQueryClient() const {openComposer} = useOpenComposer() - const [isScrolledDown, setIsScrolledDown] = React.useState(false) + const [isScrolledDown, setIsScrolledDown] = useState(false) const setMinimalShellMode = useSetMinimalShellMode() const headerOffset = useHeaderOffset() const feedFeedback = useFeedFeedback(feed, hasSession) - const scrollElRef = React.useRef<ListMethods>(null) - const [hasNew, setHasNew] = React.useState(false) + const scrollElRef = useRef<ListMethods>(null) + const [hasNew, setHasNew] = useState(false) const setHomeBadge = useSetHomeBadge() - const isVideoFeed = React.useMemo(() => { + const isVideoFeed = useMemo(() => { const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri) const feedIsVideoMode = feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO @@ -73,13 +73,13 @@ export function FeedPage({ return isNative && _isVideoFeed }, [feedInfo]) - React.useEffect(() => { + useEffect(() => { if (isPageFocused) { setHomeBadge(hasNew) } }, [isPageFocused, hasNew, setHomeBadge]) - const scrollToTop = React.useCallback(() => { + const scrollToTop = useCallback(() => { scrollElRef.current?.scrollToOffset({ animated: isNative, offset: -headerOffset, @@ -87,7 +87,7 @@ export function FeedPage({ setMinimalShellMode(false) }, [headerOffset, setMinimalShellMode]) - const onSoftReset = React.useCallback(() => { + const onSoftReset = useCallback(() => { const isScreenFocused = getTabState(getRootNavigation(navigation).getState(), 'Home') === TabState.InsideAtRoot @@ -101,21 +101,21 @@ export function FeedPage({ reason: 'soft-reset', }) } - }, [navigation, isPageFocused, scrollToTop, queryClient, feed, setHasNew]) + }, [navigation, isPageFocused, scrollToTop, queryClient, feed]) // fires when page within screen is activated/deactivated - React.useEffect(() => { + useEffect(() => { if (!isPageFocused) { return } return listenSoftReset(onSoftReset) }, [onSoftReset, isPageFocused]) - const onPressCompose = React.useCallback(() => { + const onPressCompose = useCallback(() => { openComposer({}) }, [openComposer]) - const onPressLoadLatest = React.useCallback(() => { + const onPressLoadLatest = useCallback(() => { scrollToTop() truncateAndInvalidate(queryClient, FEED_RQKEY(feed)) setHasNew(false) @@ -124,7 +124,7 @@ export function FeedPage({ feedUrl: feed, reason: 'load-latest', }) - }, [scrollToTop, feed, queryClient, setHasNew]) + }, [scrollToTop, feed, queryClient]) const shouldPrefetch = isNative && isPageAdjacent return ( diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 9aa4512a4..90ad2a522 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -1,4 +1,4 @@ -import React, {memo, useCallback, useRef} from 'react' +import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import { ActivityIndicator, AppState, @@ -22,6 +22,7 @@ import {useQueryClient} from '@tanstack/react-query' import {isStatusStillActive, validateStatus} from '#/lib/actor-status' import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isIOS, isNative, isWeb} from '#/platform/detection' @@ -208,15 +209,14 @@ let PostFeed = ({ const {currentAccount, hasSession} = useSession() const initialNumToRender = useInitialNumToRender() const feedFeedback = useFeedFeedbackContext() - const [isPTRing, setIsPTRing] = React.useState(false) - const checkForNewRef = React.useRef<(() => void) | null>(null) - const lastFetchRef = React.useRef<number>(Date.now()) + const [isPTRing, setIsPTRing] = useState(false) + const lastFetchRef = useRef<number>(Date.now()) const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') const {gtMobile} = useBreakpoints() const {rightNavVisible} = useLayoutBreakpoints() const areVideoFeedsEnabled = isNative - const [hasPressedShowLessUris, setHasPressedShowLessUris] = React.useState( + const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState( () => new Set<string>(), ) const onPressShowLess = useCallback( @@ -231,7 +231,7 @@ let PostFeed = ({ ) const feedCacheKey = feedParams?.feedCacheKey - const opts = React.useMemo( + const opts = useMemo( () => ({enabled, ignoreFilterFor}), [enabled, ignoreFilterFor], ) @@ -250,20 +250,21 @@ let PostFeed = ({ if (lastFetchedAt) { lastFetchRef.current = lastFetchedAt } - const isEmpty = React.useMemo( + const isEmpty = useMemo( () => !isFetching && !data?.pages?.some(page => page.slices.length), [isFetching, data], ) - const checkForNew = React.useCallback(async () => { + const checkForNew = useNonReactiveCallback(async () => { + if (!data?.pages[0] || isFetching || !onHasNew || !enabled || disablePoll) { + return + } + // Discover always has fresh content if (feedUriOrActorDid === DISCOVER_FEED_URI) { - return onHasNew?.(true) + return onHasNew(true) } - if (!data?.pages[0] || isFetching || !onHasNew || !enabled || disablePoll) { - return - } try { if (await pollLatest(data.pages[0])) { if (isEmpty) { @@ -275,20 +276,10 @@ let PostFeed = ({ } catch (e) { logger.error('Poll latest failed', {feed, message: String(e)}) } - }, [ - feed, - data, - isFetching, - isEmpty, - onHasNew, - enabled, - disablePoll, - refetch, - feedUriOrActorDid, - ]) + }) const myDid = currentAccount?.did || '' - const onPostCreated = React.useCallback(() => { + const onPostCreated = useCallback(() => { // NOTE // only invalidate if there's 1 page // more than 1 page can trigger some UI freakouts on iOS and android @@ -301,46 +292,41 @@ let PostFeed = ({ queryClient.invalidateQueries({queryKey: RQKEY(feed)}) } }, [queryClient, feed, data, myDid]) - React.useEffect(() => { + useEffect(() => { return listenPostCreated(onPostCreated) }, [onPostCreated]) - React.useEffect(() => { - // we store the interval handler in a ref to avoid needless - // reassignments in other effects - checkForNewRef.current = checkForNew - }, [checkForNew]) - React.useEffect(() => { + useEffect(() => { if (enabled && !disablePoll) { const timeSinceFirstLoad = Date.now() - lastFetchRef.current - if ( - (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) && - checkForNewRef.current - ) { + if (isEmpty || timeSinceFirstLoad > CHECK_LATEST_AFTER) { // check for new on enable (aka on focus) - checkForNewRef.current() + checkForNew() } } - }, [enabled, disablePoll, feed, queryClient, scrollElRef, isEmpty]) - React.useEffect(() => { + }, [enabled, isEmpty, disablePoll, checkForNew]) + + useEffect(() => { let cleanup1: () => void | undefined, cleanup2: () => void | undefined const subscription = AppState.addEventListener('change', nextAppState => { // check for new on app foreground if (nextAppState === 'active') { - checkForNewRef.current?.() + checkForNew() } }) cleanup1 = () => subscription.remove() if (pollInterval) { // check for new on interval - const i = setInterval(() => checkForNewRef.current?.(), pollInterval) + const i = setInterval(() => { + checkForNew() + }, pollInterval) cleanup2 = () => clearInterval(i) } return () => { cleanup1?.() cleanup2?.() } - }, [pollInterval]) + }, [pollInterval, checkForNew]) const followProgressGuide = useProgressGuide('follow-10') const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7') @@ -350,7 +336,7 @@ let PostFeed = ({ const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings() - const feedItems: FeedRow[] = React.useMemo(() => { + const feedItems: FeedRow[] = useMemo(() => { // wraps a slice item, and replaces it with a showLessFollowup item // if the user has pressed show less on it const sliceItem = (row: Extract<FeedRow, {type: 'sliceItem'}>) => { @@ -407,6 +393,7 @@ let PostFeed = ({ for (const page of data.pages) { for (const slice of page.slices) { const item = slice.items.find( + // eslint-disable-next-line @typescript-eslint/no-shadow item => item.uri === slice.feedPostUri, ) if (item && AppBskyEmbedVideo.isView(item.post.embed)) { @@ -599,7 +586,7 @@ let PostFeed = ({ // events // = - const onRefresh = React.useCallback(async () => { + const onRefresh = useCallback(async () => { logEvent('feed:refresh', { feedType: feedType, feedUrl: feed, @@ -615,7 +602,7 @@ let PostFeed = ({ setIsPTRing(false) }, [refetch, setIsPTRing, onHasNew, feed, feedType]) - const onEndReached = React.useCallback(async () => { + const onEndReached = useCallback(async () => { if (isFetching || !hasNextPage || isError) return logEvent('feed:endReached', { @@ -638,19 +625,19 @@ let PostFeed = ({ feedItems.length, ]) - const onPressTryAgain = React.useCallback(() => { + const onPressTryAgain = useCallback(() => { refetch() onHasNew?.(false) }, [refetch, onHasNew]) - const onPressRetryLoadMore = React.useCallback(() => { + const onPressRetryLoadMore = useCallback(() => { fetchNextPage() }, [fetchNextPage]) // rendering // = - const renderItem = React.useCallback( + const renderItem = useCallback( ({item: row, index: rowIndex}: ListRenderItemInfo<FeedRow>) => { if (row.type === 'empty') { return renderEmptyState() @@ -773,7 +760,7 @@ let PostFeed = ({ const shouldRenderEndOfFeed = !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed - const FeedFooter = React.useCallback(() => { + const FeedFooter = useCallback(() => { /** * A bit of padding at the bottom of the feed as you scroll and when you * reach the end, so that content isn't cut off by the bottom of the diff --git a/src/view/com/util/load-latest/LoadLatestBtn.tsx b/src/view/com/util/load-latest/LoadLatestBtn.tsx index f991991b0..8b9d0e359 100644 --- a/src/view/com/util/load-latest/LoadLatestBtn.tsx +++ b/src/view/com/util/load-latest/LoadLatestBtn.tsx @@ -1,19 +1,20 @@ -import {StyleSheet, View} from 'react-native' +import {StyleSheet} from 'react-native' import Animated from 'react-native-reanimated' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useMediaQuery} from 'react-responsive' import {HITSLOP_20} from '#/lib/constants' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' -import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {clamp} from '#/lib/numbers' import {useGate} from '#/lib/statsig/statsig' -import {colors} from '#/lib/styles' import {useSession} from '#/state/session' -import {atoms as a, useLayoutBreakpoints} from '#/alf' +import {atoms as a, useLayoutBreakpoints, useTheme, web} from '#/alf' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {ArrowTop_Stroke2_Corner0_Rounded as ArrowIcon} from '#/components/icons/Arrow' +import {CENTER_COLUMN_OFFSET} from '#/components/Layout' +import {SubtleWebHover} from '#/components/SubtleWebHover' export function LoadLatestBtn({ onPress, @@ -24,12 +25,17 @@ export function LoadLatestBtn({ label: string showIndicator: boolean }) { - const pal = usePalette('default') const {hasSession} = useSession() const {isDesktop, isTablet, isMobile, isTabletOrMobile} = useWebMediaQueries() const {centerColumnOffset} = useLayoutBreakpoints() const fabMinimalShellTransform = useMinimalShellFabTransform() const insets = useSafeAreaInsets() + const t = useTheme() + const { + state: hovered, + onIn: onHoverIn, + onOut: onHoverOut, + } = useInteractionState() // move button inline if it starts overlapping the left nav const isTallViewport = useMediaQuery({minHeight: 700}) @@ -48,67 +54,66 @@ export function LoadLatestBtn({ : {bottom: clamp(insets.bottom, 15, 60) + 15} return ( - <Animated.View style={[showBottomBar && fabMinimalShellTransform]}> + <Animated.View + testID="loadLatestBtn" + style={[ + a.fixed, + a.z_20, + {left: 18}, + isDesktop && + (isTallViewport + ? styles.loadLatestOutOfLine + : styles.loadLatestInline), + isTablet && + (centerColumnOffset + ? styles.loadLatestInlineOffset + : styles.loadLatestInline), + bottomPosition, + showBottomBar && fabMinimalShellTransform, + ]}> <PressableScale style={[ - styles.loadLatest, - isDesktop && - (isTallViewport - ? styles.loadLatestOutOfLine - : styles.loadLatestInline), - isTablet && - (centerColumnOffset - ? styles.loadLatestInlineOffset - : styles.loadLatestInline), - pal.borderDark, - pal.view, - bottomPosition, + { + width: 42, + height: 42, + }, + a.rounded_full, + a.align_center, + a.justify_center, + a.border, + t.atoms.border_contrast_low, + showIndicator ? {backgroundColor: t.palette.primary_50} : t.atoms.bg, ]} onPress={onPress} hitSlop={HITSLOP_20} accessibilityLabel={label} accessibilityHint="" - targetScale={0.9}> - <FontAwesomeIcon icon="angle-up" color={pal.colors.text} size={19} /> - {showIndicator && <View style={[styles.indicator, pal.borderDark]} />} + targetScale={0.9} + onPointerEnter={onHoverIn} + onPointerLeave={onHoverOut}> + <SubtleWebHover hover={hovered} style={[a.rounded_full]} /> + <ArrowIcon + size="md" + style={[ + a.z_10, + showIndicator + ? {color: t.palette.primary_500} + : t.atoms.text_contrast_medium, + ]} + /> </PressableScale> </Animated.View> ) } const styles = StyleSheet.create({ - loadLatest: { - zIndex: 20, - ...a.fixed, - left: 18, - borderWidth: StyleSheet.hairlineWidth, - width: 52, - height: 52, - borderRadius: 26, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, loadLatestInline: { - // @ts-expect-error web only - left: 'calc(50vw - 282px)', + left: web('calc(50vw - 282px)'), }, loadLatestInlineOffset: { - // @ts-expect-error web only - left: 'calc(50vw - 432px)', + left: web(`calc(50vw - 282px + ${CENTER_COLUMN_OFFSET}px)`), }, loadLatestOutOfLine: { - // @ts-expect-error web only - left: 'calc(50vw - 382px)', - }, - indicator: { - position: 'absolute', - top: 3, - right: 3, - backgroundColor: colors.blue3, - width: 12, - height: 12, - borderRadius: 6, - borderWidth: 1, + left: web('calc(50vw - 382px)'), }, }) |