Files
org-social-ios/App/ViewModels/InteractionSnapshotStore.swift
andros 7631ca0e6e Improve timeline scroll performance
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.
2026-07-07 20:42:46 +02:00

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
}
}