diff --git a/assets/icons/arrowTop_stroke2_corner0_rounded.svg b/assets/icons/arrowTop_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..bf6f0c1122
--- /dev/null
+++ b/assets/icons/arrowTop_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/SubtleWebHover.web.tsx b/src/components/SubtleWebHover.web.tsx
index 8943147e49..af00cf43a0 100644
--- a/src/components/SubtleWebHover.web.tsx
+++ b/src/components/SubtleWebHover.web.tsx
@@ -1,7 +1,7 @@
import {StyleSheet, View} from 'react-native'
import {isTouchDevice} from '#/lib/browser'
-import {useTheme, ViewStyleProp} from '#/alf'
+import {useTheme, type ViewStyleProp} from '#/alf'
export function SubtleWebHover({
style,
diff --git a/src/components/icons/Arrow.tsx b/src/components/icons/Arrow.tsx
index 0d4bc9479e..57a313cc45 100644
--- a/src/components/icons/Arrow.tsx
+++ b/src/components/icons/Arrow.tsx
@@ -4,6 +4,10 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8 6a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v9a1 1 0 1 1-2 0V8.414l-9.793 9.793a1 1 0 0 1-1.414-1.414L15.586 7H9a1 1 0 0 1-1-1Z',
})
+export const ArrowTop_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M11 20V6.164l-4.293 4.293a1 1 0 1 1-1.414-1.414l5.293-5.293.151-.138a2 2 0 0 1 2.677.138l5.293 5.293.068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 6.164V20a1 1 0 0 1-2 0Z',
+})
+
export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z',
})
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 604533b0f1..e8a177a8d8 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>()
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(null)
- const [hasNew, setHasNew] = React.useState(false)
+ const scrollElRef = useRef(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 9aa4512a4d..90ad2a5228 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(Date.now())
+ const [isPTRing, setIsPTRing] = useState(false)
+ const lastFetchRef = useRef(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(),
)
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 () => {
- // Discover always has fresh content
- if (feedUriOrActorDid === DISCOVER_FEED_URI) {
- return onHasNew?.(true)
- }
-
+ 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)
+ }
+
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) => {
@@ -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) => {
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 f991991b0e..8b9d0e3593 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 (
-
+
-
- {showIndicator && }
+ targetScale={0.9}
+ onPointerEnter={onHoverIn}
+ onPointerLeave={onHoverOut}>
+
+
)
}
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)'),
},
})