Files
hapi/ios/Hapi/Features/Chat/ChatPresentationState.swift
T
weishu c2e3d16b7e feat(native): improve anchored chat scrolling and history loading
Add viewport-driven paging with layout acknowledgements, bounded retries, cancellation gates, and epoch-safe history retention.

Preserve transcript anchors and expansion state, fix tool-group identity collisions, and serialize Android history coordination on Main.

Reduce per-scroll composition and layout work; add native regression tests, CI coverage, and profiling guidance.
2026-09-10 16:22:36 +08:00

47 lines
1.4 KiB
Swift

import Observation
import SwiftUI
/// User-owned state outlives recycled cells, but not the retained transcript.
@MainActor @Observable
final class ChatPresentationState {
struct Key: Hashable { let id: String; let field: String }
var values: [Key: Any] = [:]
func prune(to ids: Set<String>) {
for key in values.keys.filter({ !ids.contains($0.id) }) { values.removeValue(forKey: key) }
}
}
private struct ChatPresentationStateKey: EnvironmentKey {
static let defaultValue: ChatPresentationState? = nil
}
extension EnvironmentValues {
var chatPresentationState: ChatPresentationState? {
get { self[ChatPresentationStateKey.self] }
set { self[ChatPresentationStateKey.self] = newValue }
}
}
@MainActor @propertyWrapper
struct ChatStoredState<Value>: DynamicProperty {
@Environment(\.chatPresentationState) private var store
@State private var fallback: Value
private let key: ChatPresentationState.Key
init(wrappedValue: Value, id: String, field: String) {
_fallback = State(initialValue: wrappedValue)
key = .init(id: id, field: field)
}
var wrappedValue: Value {
get { store?.values[key] as? Value ?? fallback }
nonmutating set {
if let store { store.values[key] = newValue }
else { fallback = newValue }
}
}
var projectedValue: Binding<Value> { Binding(get: { wrappedValue }, set: { wrappedValue = $0 }) }
}