diff --git a/src/lib/api/build-suggested-posts.ts b/src/lib/api/build-suggested-posts.ts
deleted file mode 100644
index 554869d9..00000000
--- a/src/lib/api/build-suggested-posts.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import {RootStoreModel} from 'state/index'
-import {
- AppBskyFeedDefs,
- AppBskyFeedGetAuthorFeed as GetAuthorFeed,
-} from '@atproto/api'
-type ReasonRepost = AppBskyFeedDefs.ReasonRepost
-
-async function getMultipleAuthorsPosts(
- rootStore: RootStoreModel,
- authors: string[],
- cursor: string | undefined = undefined,
- limit: number = 10,
-) {
- const responses = await Promise.all(
- authors.map((actor, index) =>
- rootStore.agent
- .getAuthorFeed({
- actor,
- limit,
- cursor: cursor ? cursor.split(',')[index] : undefined,
- })
- .catch(_err => ({success: false, headers: {}, data: {feed: []}})),
- ),
- )
- return responses
-}
-
-function mergePosts(
- responses: GetAuthorFeed.Response[],
- {repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
-) {
- let posts: AppBskyFeedDefs.FeedViewPost[] = []
-
- if (bestOfOnly) {
- for (const res of responses) {
- if (res.success) {
- // filter the feed down to the post with the most likes
- res.data.feed = res.data.feed.reduce(
- (acc: AppBskyFeedDefs.FeedViewPost[], v) => {
- if (
- !acc?.[0] &&
- !v.reason &&
- !v.reply &&
- isRecentEnough(v.post.indexedAt)
- ) {
- return [v]
- }
- if (
- acc &&
- !v.reason &&
- !v.reply &&
- (v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) &&
- isRecentEnough(v.post.indexedAt)
- ) {
- return [v]
- }
- return acc
- },
- [],
- )
- }
- }
- }
-
- // merge into one array
- for (const res of responses) {
- if (res.success) {
- posts = posts.concat(res.data.feed)
- }
- }
-
- // filter down to reposts of other users
- const uris = new Set()
- posts = posts.filter(p => {
- if (repostsOnly && !isARepostOfSomeoneElse(p)) {
- return false
- }
- if (uris.has(p.post.uri)) {
- return false
- }
- uris.add(p.post.uri)
- return true
- })
-
- // sort by index time
- posts.sort((a, b) => {
- return (
- Number(new Date(b.post.indexedAt)) - Number(new Date(a.post.indexedAt))
- )
- })
-
- return posts
-}
-
-function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean {
- return (
- post.reason?.$type === 'app.bsky.feed.defs#reasonRepost' &&
- post.post.author.did !== (post.reason as ReasonRepost).by.did
- )
-}
-
-function getCombinedCursors(responses: GetAuthorFeed.Response[]) {
- let hasCursor = false
- const cursors = responses.map(r => {
- if (r.data.cursor) {
- hasCursor = true
- return r.data.cursor
- }
- return ''
- })
- if (!hasCursor) {
- return undefined
- }
- const combinedCursors = cursors.join(',')
- return combinedCursors
-}
-
-function isCombinedCursor(cursor: string) {
- return cursor.includes(',')
-}
-
-const TWO_DAYS_AGO = Date.now() - 1e3 * 60 * 60 * 48
-function isRecentEnough(date: string) {
- try {
- const d = Number(new Date(date))
- return d > TWO_DAYS_AGO
- } catch {
- return false
- }
-}
-
-export {
- getMultipleAuthorsPosts,
- mergePosts,
- getCombinedCursors,
- isCombinedCursor,
-}
diff --git a/src/lib/hooks/useTimer.ts b/src/lib/hooks/useTimer.ts
new file mode 100644
index 00000000..bf3ecc07
--- /dev/null
+++ b/src/lib/hooks/useTimer.ts
@@ -0,0 +1,32 @@
+import * as React from 'react'
+
+/**
+ * Helper hook to run persistent timers on views
+ */
+export function useTimer(time: number, handler: () => void) {
+ const timer = React.useRef(undefined)
+
+ // function to restart the timer
+ const reset = React.useCallback(() => {
+ if (timer.current) {
+ clearTimeout(timer.current)
+ }
+ timer.current = setTimeout(handler, time)
+ }, [time, timer, handler])
+
+ // function to cancel the timer
+ const cancel = React.useCallback(() => {
+ if (timer.current) {
+ clearTimeout(timer.current)
+ timer.current = undefined
+ }
+ }, [timer])
+
+ // start the timer immediately
+ React.useEffect(() => {
+ reset()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return [reset, cancel]
+}
diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx
index 8fa8e11d..dab95035 100644
--- a/src/lib/icons.tsx
+++ b/src/lib/icons.tsx
@@ -801,8 +801,8 @@ export function SquarePlusIcon({
height={size || 24}
style={style}>
{
params = Object.assign({}, this.params, params)
- if (this.feedType === 'suggested') {
- const responses = await getMultipleAuthorsPosts(
- this.rootStore,
- sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
- params.cursor,
- 20,
- )
- const combinedCursor = getCombinedCursors(responses)
- const finalData = mergePosts(responses, {bestOfOnly: true})
- const lastHeaders = responses[responses.length - 1].headers
- return {
- success: true,
- data: {
- feed: finalData,
- cursor: combinedCursor,
- },
- headers: lastHeaders,
- }
- } else if (this.feedType === 'home') {
+ if (this.feedType === 'home') {
return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
} else if (this.feedType === 'custom') {
- return this.rootStore.agent.app.bsky.feed.getFeed(
+ const res = await this.rootStore.agent.app.bsky.feed.getFeed(
params as GetCustomFeed.QueryParams,
)
+ // NOTE
+ // some custom feeds fail to enforce the pagination limit
+ // so we manually truncate here
+ // -prf
+ if (params.limit && res.data.feed.length > params.limit) {
+ res.data.feed = res.data.feed.slice(0, params.limit)
+ }
+ return res
} else {
return this.rootStore.agent.getAuthorFeed(
params as GetAuthorFeed.QueryParams,
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 169f8879..7d445238 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -14,11 +14,13 @@ import {PostsMultiFeedModel} from 'state/models/feeds/multi-feed'
import {MultiFeed} from 'view/com/posts/MultiFeed'
import {isDesktopWeb} from 'platform/detection'
import {usePalette} from 'lib/hooks/usePalette'
+import {useTimer} from 'lib/hooks/useTimer'
import {useStores} from 'state/index'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {ComposeIcon2, CogIcon} from 'lib/icons'
import {s} from 'lib/styles'
+const LOAD_NEW_PROMPT_TIME = 60e3 // 60 seconds
const HEADER_OFFSET = isDesktopWeb ? 0 : 40
type Props = NativeStackScreenProps
@@ -33,11 +35,24 @@ export const FeedsScreen = withAuthRequired(
)
const [onMainScroll, isScrolledDown, resetMainScroll] =
useOnMainScroll(store)
+ const [loadPromptVisible, setLoadPromptVisible] = React.useState(false)
+ const [resetPromptTimer] = useTimer(LOAD_NEW_PROMPT_TIME, () => {
+ setLoadPromptVisible(true)
+ })
const onSoftReset = React.useCallback(() => {
flatListRef.current?.scrollToOffset({offset: 0})
+ multifeed.loadLatest()
+ resetPromptTimer()
+ setLoadPromptVisible(false)
resetMainScroll()
- }, [flatListRef, resetMainScroll])
+ }, [
+ flatListRef,
+ resetMainScroll,
+ multifeed,
+ resetPromptTimer,
+ setLoadPromptVisible,
+ ])
useFocusEffect(
React.useCallback(() => {
@@ -99,11 +114,11 @@ export const FeedsScreen = withAuthRequired(
hideOnScroll
renderButton={renderHeaderBtn}
/>
- {isScrolledDown ? (
+ {isScrolledDown || loadPromptVisible ? (
) : null}
{