Pinafore-Web-Client-Frontend/src/routes/_utils/sortItemSummariesForThread.js

74 lines
2.5 KiB
JavaScript

// This is designed to exactly mimic Mastodon's ordering for threads. As described by Gargron:
// "statuses are ordered in the postgresql query and then any of OP's self-replies bubble to the top"
// Source: https://github.com/tootsuite/mastodon/blob/ef15246/app/models/concerns/status_threading_concern.rb
import { concat } from './arrays.js'
import { compareTimelineItemSummaries } from './statusIdSorting.js'
import { mapBy, multimapBy } from './maps.js'
export function sortItemSummariesForThread (summaries, statusId) {
const ancestors = []
const descendants = []
const summariesById = mapBy(summaries, _ => _.id)
const summariesByReplyId = multimapBy(summaries, _ => _.replyId)
const status = summariesById.get(statusId)
if (!status) {
// bail out, for some reason we can't find the status (should never happen)
return summaries
}
// find ancestors
let currentStatus = status
do {
currentStatus = summariesById.get(currentStatus.replyId)
if (currentStatus) {
ancestors.unshift(currentStatus)
}
} while (currentStatus)
// find descendants
// This mirrors the depth-first ordering used in the Postgres query in the Mastodon implementation
const stack = [status]
while (stack.length) {
const current = stack.shift()
const newChildren = (summariesByReplyId.get(current.id) || []).sort(compareTimelineItemSummaries)
Array.prototype.unshift.apply(stack, newChildren)
if (current.id !== status.id) { // the status is not a descendant of itself
descendants.push(current)
}
}
// Normally descendants are sorted in depth-first order, via normal ID sorting
// but replies that come from the account they're actually replying to get promoted
// This only counts if it's an unbroken self-reply, e.g. in the case of
// A -> A -> A -> B -> A -> A
// B has broken the chain, so only the first three As are considered unbroken self-replies
const isUnbrokenSelfReply = (descendant) => {
let current = descendant
while (true) {
if (current.accountId !== status.accountId) {
return false
}
const parent = summariesById.get(current.replyId)
if (!parent) {
break
}
current = parent
}
return current.id === statusId
}
const promotedDescendants = []
const otherDescendants = []
for (const descendant of descendants) {
(isUnbrokenSelfReply(descendant) ? promotedDescendants : otherDescendants).push(descendant)
}
return concat(
ancestors,
[status],
promotedDescendants,
otherDescendants
)
}