about summary refs log tree commit diff
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-11-24 19:10:54 +0000
committerGitHub <noreply@github.com>2024-11-24 19:10:54 +0000
commitb9406aa011db2db7d3bcdf7d91ef929b17a07c02 (patch)
treefe590626f573c2e78e4d9f7df73f33c523e62172
parent371e6ad333e00cdfb7f4713f7fef816668011049 (diff)
downloadvoidsky-b9406aa011db2db7d3bcdf7d91ef929b17a07c02.tar.zst
Don't re-sort already fetched post thread items (#6698)
* Don't reorder already seen posts in PostThread

* Add sorting by generation

* Rip out stable order cache

It doesn't make sense because sort() doesn't call the callback for all A/B pairs, and the server returning a different ordering will cause cache misses which means there'll be no stability anyway.

* Make hotness deterministic per fetched at

* Cache random scores while in thread

* Reorder for clarity
-rw-r--r--src/state/queries/post-thread.ts52
-rw-r--r--src/view/com/post-thread/PostThread.tsx9
2 files changed, 52 insertions, 9 deletions
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 4784a9d75..2ebaf4f7d 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -150,6 +150,9 @@ export function sortThread(
   currentDid: string | undefined,
   justPostedUris: Set<string>,
   threadgateRecordHiddenReplies: Set<string>,
+  fetchedAtCache: Map<string, number>,
+  fetchedAt: number,
+  randomCache: Map<string, number>,
 ): ThreadNode {
   if (node.type !== 'post') {
     return node
@@ -237,9 +240,23 @@ export function sortThread(
         }
       }
 
-      if (opts.sort === 'hotness') {
-        const aHotness = getHotness(a.post)
-        const bHotness = getHotness(b.post)
+      // Split items from different fetches into separate generations.
+      let aFetchedAt = fetchedAtCache.get(a.uri)
+      if (aFetchedAt === undefined) {
+        fetchedAtCache.set(a.uri, fetchedAt)
+        aFetchedAt = fetchedAt
+      }
+      let bFetchedAt = fetchedAtCache.get(b.uri)
+      if (bFetchedAt === undefined) {
+        fetchedAtCache.set(b.uri, fetchedAt)
+        bFetchedAt = fetchedAt
+      }
+
+      if (aFetchedAt !== bFetchedAt) {
+        return aFetchedAt - bFetchedAt // older fetches first
+      } else if (opts.sort === 'hotness') {
+        const aHotness = getHotness(a.post, aFetchedAt)
+        const bHotness = getHotness(b.post, bFetchedAt /* same as aFetchedAt */)
         return bHotness - aHotness
       } else if (opts.sort === 'oldest') {
         return a.post.indexedAt.localeCompare(b.post.indexedAt)
@@ -252,9 +269,21 @@ export function sortThread(
           return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
         }
       } else if (opts.sort === 'random') {
-        return 0.5 - Math.random() // this is vaguely criminal but we can get away with it
+        let aRandomScore = randomCache.get(a.uri)
+        if (aRandomScore === undefined) {
+          aRandomScore = Math.random()
+          randomCache.set(a.uri, aRandomScore)
+        }
+        let bRandomScore = randomCache.get(b.uri)
+        if (bRandomScore === undefined) {
+          bRandomScore = Math.random()
+          randomCache.set(b.uri, bRandomScore)
+        }
+        // this is vaguely criminal but we can get away with it
+        return aRandomScore - bRandomScore
+      } else {
+        return b.post.indexedAt.localeCompare(a.post.indexedAt)
       }
-      return b.post.indexedAt.localeCompare(a.post.indexedAt)
     })
     node.replies.forEach(reply =>
       sortThread(
@@ -264,6 +293,9 @@ export function sortThread(
         currentDid,
         justPostedUris,
         threadgateRecordHiddenReplies,
+        fetchedAtCache,
+        fetchedAt,
+        randomCache,
       ),
     )
   }
@@ -277,10 +309,12 @@ export function sortThread(
 // We want to give recent comments a real chance (and not bury them deep below the fold)
 // while also surfacing well-liked comments from the past. In the future, we can explore
 // something more sophisticated, but we don't have much data on the client right now.
-function getHotness(post: AppBskyFeedDefs.PostView) {
-  const hoursAgo =
-    (new Date().getTime() - new Date(post.indexedAt).getTime()) /
-    (1000 * 60 * 60)
+function getHotness(post: AppBskyFeedDefs.PostView, fetchedAt: number) {
+  const hoursAgo = Math.max(
+    0,
+    (new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
+      (1000 * 60 * 60),
+  )
   const likeCount = post.likeCount ?? 0
   const likeOrder = Math.log(3 + likeCount)
   const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 7ce6deaa1..315800fe5 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -104,6 +104,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
     error: threadError,
     refetch,
     data: {thread, threadgate} = {},
+    dataUpdatedAt: fetchedAt,
   } = usePostThreadQuery(uri)
 
   const treeView = React.useMemo(
@@ -171,6 +172,8 @@ export function PostThread({uri}: {uri: string | undefined}) {
     () => new Set<string>(),
   )
 
+  const [fetchedAtCache] = React.useState(() => new Map<string, number>())
+  const [randomCache] = React.useState(() => new Map<string, number>())
   const skeleton = React.useMemo(() => {
     const threadViewPrefs = preferences?.threadViewPrefs
     if (!threadViewPrefs || !thread) return null
@@ -183,6 +186,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
         currentDid,
         justPostedUris,
         threadgateHiddenReplies,
+        fetchedAtCache,
+        fetchedAt,
+        randomCache,
       ),
       currentDid,
       treeView,
@@ -199,6 +205,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
     hiddenRepliesState,
     justPostedUris,
     threadgateHiddenReplies,
+    fetchedAtCache,
+    fetchedAt,
+    randomCache,
   ])
 
   const error = React.useMemo(() => {