Two changes to remove the main sources of scroll jank: - Index own interactions once per feed parse instead of per row. OwnInteractionCache.ingest bulk-imports reactions, boosts and poll votes from the user's own feed wherever it is already parsed (timeline load, mergeOwnFeed, FollowCoordinator.apply). The loadExisting* methods become synchronous cache reads, removing the full own-feed download + parse that ran on the main actor for every row that scrolled into view. Merge-only so a just-written interaction is never wiped by a CDN-stale copy. Vote detection no longer requires an empty body (the spec allows vote posts with text). - Keep row heights stable while scrolling. New InteractionSnapshotStore remembers the last interaction data per post for the session; PostRowView restores it synchronously in onAppear so rows recreated by the LazyVStack render at final height immediately instead of growing when /interactions/ answers. First-ever data arrival is animated instead of appearing abruptly.
39 lines
1.3 KiB
Swift
39 lines
1.3 KiB
Swift
import Foundation
|
|
import OrgSocialKit
|
|
|
|
/// Session-scoped memory of the last interaction data each post row showed
|
|
/// (reply count, boost count, reaction chips, poll votes).
|
|
///
|
|
/// The timeline's LazyVStack discards offscreen rows. Without this store, a
|
|
/// row recreated on scroll-back starts empty and grows a moment later when
|
|
/// `/interactions/` answers, shifting the whole viewport mid-scroll — the
|
|
/// "jumpy scroll" feel. Restoring the last-known values synchronously in
|
|
/// `onAppear` keeps the row at its final height from the first frame; the
|
|
/// network refresh still runs and updates the values in place.
|
|
///
|
|
/// This is UI-state memory, not a network cache: rows always refetch. A TTL
|
|
/// cache matching the relay's 1-minute scan cadence is a separate planned
|
|
/// improvement (see CLAUDE.md performance backlog).
|
|
@MainActor
|
|
final class InteractionSnapshotStore {
|
|
|
|
struct Snapshot {
|
|
var replyCount: Int?
|
|
var boostCount: Int
|
|
var reactions: [OrgSocialMood]
|
|
var pollVotes: [OrgSocialPollVote]
|
|
}
|
|
|
|
static let shared = InteractionSnapshotStore()
|
|
|
|
private var snapshots: [String: Snapshot] = [:]
|
|
|
|
func snapshot(for postURL: String) -> Snapshot? {
|
|
snapshots[postURL]
|
|
}
|
|
|
|
func store(_ snapshot: Snapshot, for postURL: String) {
|
|
snapshots[postURL] = snapshot
|
|
}
|
|
}
|