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
This commit is contained in:
@@ -150,6 +150,9 @@ export function sortThread(
|
|||||||
currentDid: string | undefined,
|
currentDid: string | undefined,
|
||||||
justPostedUris: Set<string>,
|
justPostedUris: Set<string>,
|
||||||
threadgateRecordHiddenReplies: Set<string>,
|
threadgateRecordHiddenReplies: Set<string>,
|
||||||
|
fetchedAtCache: Map<string, number>,
|
||||||
|
fetchedAt: number,
|
||||||
|
randomCache: Map<string, number>,
|
||||||
): ThreadNode {
|
): ThreadNode {
|
||||||
if (node.type !== 'post') {
|
if (node.type !== 'post') {
|
||||||
return node
|
return node
|
||||||
@@ -237,9 +240,23 @@ export function sortThread(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.sort === 'hotness') {
|
// Split items from different fetches into separate generations.
|
||||||
const aHotness = getHotness(a.post)
|
let aFetchedAt = fetchedAtCache.get(a.uri)
|
||||||
const bHotness = getHotness(b.post)
|
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
|
return bHotness - aHotness
|
||||||
} else if (opts.sort === 'oldest') {
|
} else if (opts.sort === 'oldest') {
|
||||||
return a.post.indexedAt.localeCompare(b.post.indexedAt)
|
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
|
return (b.post.likeCount || 0) - (a.post.likeCount || 0) // most likes
|
||||||
}
|
}
|
||||||
} else if (opts.sort === 'random') {
|
} 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 =>
|
node.replies.forEach(reply =>
|
||||||
sortThread(
|
sortThread(
|
||||||
@@ -264,6 +293,9 @@ export function sortThread(
|
|||||||
currentDid,
|
currentDid,
|
||||||
justPostedUris,
|
justPostedUris,
|
||||||
threadgateRecordHiddenReplies,
|
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)
|
// 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
|
// 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.
|
// something more sophisticated, but we don't have much data on the client right now.
|
||||||
function getHotness(post: AppBskyFeedDefs.PostView) {
|
function getHotness(post: AppBskyFeedDefs.PostView, fetchedAt: number) {
|
||||||
const hoursAgo =
|
const hoursAgo = Math.max(
|
||||||
(new Date().getTime() - new Date(post.indexedAt).getTime()) /
|
0,
|
||||||
(1000 * 60 * 60)
|
(new Date(fetchedAt).getTime() - new Date(post.indexedAt).getTime()) /
|
||||||
|
(1000 * 60 * 60),
|
||||||
|
)
|
||||||
const likeCount = post.likeCount ?? 0
|
const likeCount = post.likeCount ?? 0
|
||||||
const likeOrder = Math.log(3 + likeCount)
|
const likeOrder = Math.log(3 + likeCount)
|
||||||
const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
|
const timePenaltyExponent = 1.5 + 1.5 / (1 + Math.log(1 + likeCount))
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
|||||||
error: threadError,
|
error: threadError,
|
||||||
refetch,
|
refetch,
|
||||||
data: {thread, threadgate} = {},
|
data: {thread, threadgate} = {},
|
||||||
|
dataUpdatedAt: fetchedAt,
|
||||||
} = usePostThreadQuery(uri)
|
} = usePostThreadQuery(uri)
|
||||||
|
|
||||||
const treeView = React.useMemo(
|
const treeView = React.useMemo(
|
||||||
@@ -171,6 +172,8 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
|||||||
() => new Set<string>(),
|
() => new Set<string>(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [fetchedAtCache] = React.useState(() => new Map<string, number>())
|
||||||
|
const [randomCache] = React.useState(() => new Map<string, number>())
|
||||||
const skeleton = React.useMemo(() => {
|
const skeleton = React.useMemo(() => {
|
||||||
const threadViewPrefs = preferences?.threadViewPrefs
|
const threadViewPrefs = preferences?.threadViewPrefs
|
||||||
if (!threadViewPrefs || !thread) return null
|
if (!threadViewPrefs || !thread) return null
|
||||||
@@ -183,6 +186,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
|||||||
currentDid,
|
currentDid,
|
||||||
justPostedUris,
|
justPostedUris,
|
||||||
threadgateHiddenReplies,
|
threadgateHiddenReplies,
|
||||||
|
fetchedAtCache,
|
||||||
|
fetchedAt,
|
||||||
|
randomCache,
|
||||||
),
|
),
|
||||||
currentDid,
|
currentDid,
|
||||||
treeView,
|
treeView,
|
||||||
@@ -199,6 +205,9 @@ export function PostThread({uri}: {uri: string | undefined}) {
|
|||||||
hiddenRepliesState,
|
hiddenRepliesState,
|
||||||
justPostedUris,
|
justPostedUris,
|
||||||
threadgateHiddenReplies,
|
threadgateHiddenReplies,
|
||||||
|
fetchedAtCache,
|
||||||
|
fetchedAt,
|
||||||
|
randomCache,
|
||||||
])
|
])
|
||||||
|
|
||||||
const error = React.useMemo(() => {
|
const error = React.useMemo(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user