Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15efbcc205 |
@@ -45,25 +45,9 @@ private struct AgentTranscriptView: View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 15) {
|
||||
if allMessages.count > displayed.count {
|
||||
Button("Load earlier messages") {
|
||||
// Loading older history may move the viewport as lazy
|
||||
// row estimates settle; do not chase the old position.
|
||||
atLatest = false
|
||||
historyLimit += 100
|
||||
historyStartID = allMessages[max(0, startIndex - 100)].id
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
if room.isLoading { ProgressView("Loading messages…").frame(maxWidth: .infinity) }
|
||||
ForEach(Array(displayed.enumerated()), id: \.element.id) { index, message in
|
||||
AgentTranscriptRow(message: message, room: room,
|
||||
startsDay: index == 0 || !Calendar.current.isDate(message.createdAt, inSameDayAs: displayed[index - 1].createdAt))
|
||||
.equatable()
|
||||
.modifier(AgentEntrance(animate: appeared && rendered.entranceIDs.contains(message.id), reduceMotion: reduceMotion))
|
||||
.id(message.id)
|
||||
}
|
||||
// The native scroll origin is the visual bottom. Keep model
|
||||
// and cursor order ascending; reverse only presentation.
|
||||
Color.clear.frame(height: 17).id(Self.bottomID)
|
||||
if rendered.isTyping {
|
||||
HStack(spacing: 9) {
|
||||
AgentAvatar()
|
||||
@@ -72,33 +56,49 @@ private struct AgentTranscriptView: View {
|
||||
}
|
||||
.foregroundStyle(SybilTheme.textMuted)
|
||||
.accessibilityIdentifier("agent-typing")
|
||||
.scaleEffect(x: 1, y: -1)
|
||||
}
|
||||
ForEach(Array(displayed.enumerated().reversed()), id: \.element.id) { index, message in
|
||||
AgentTranscriptRow(message: message, room: room,
|
||||
startsDay: index == 0 || !Calendar.current.isDate(message.createdAt, inSameDayAs: displayed[index - 1].createdAt))
|
||||
.equatable()
|
||||
.modifier(AgentEntrance(animate: appeared && rendered.entranceIDs.contains(message.id), reduceMotion: reduceMotion))
|
||||
.scaleEffect(x: 1, y: -1)
|
||||
.id(message.id)
|
||||
}
|
||||
if room.isLoading {
|
||||
ProgressView("Loading messages…").frame(maxWidth: .infinity)
|
||||
.scaleEffect(x: 1, y: -1)
|
||||
}
|
||||
if allMessages.count > displayed.count {
|
||||
Button("Load earlier messages") {
|
||||
atLatest = false
|
||||
historyLimit += 100
|
||||
historyStartID = allMessages[max(0, startIndex - 100)].id
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.scaleEffect(x: 1, y: -1)
|
||||
}
|
||||
// Include bottom padding in the target so Latest reaches
|
||||
// the actual content end before native keyboard avoidance.
|
||||
Color.clear.frame(height: 17).id(Self.bottomID)
|
||||
}
|
||||
// Keep native lazy-row targets without binding scroll position.
|
||||
.scrollTargetLayout()
|
||||
.padding(.horizontal, 16)
|
||||
// Match normal chat: content clears the header, while the
|
||||
// viewport extends beneath the shared navigation fade.
|
||||
.padding(.top, 16 + topInset)
|
||||
.padding(.bottom, 16 + topInset)
|
||||
.frame(maxWidth: SybilLayout.webContentMaxWidth)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.scaleEffect(x: 1, y: -1)
|
||||
.accessibilityIdentifier("agent-transcript")
|
||||
.defaultScrollAnchor(.bottom, for: .initialOffset)
|
||||
// Let SwiftUI follow size changes, including keyboard avoidance,
|
||||
// without competing scrollTo calls from geometry updates.
|
||||
.defaultScrollAnchor(atLatest ? .bottom : nil, for: .sizeChanges)
|
||||
.defaultScrollAnchor(.bottom, for: .alignment)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onScrollPhaseChange { _, phase in
|
||||
scrollPhase = phase
|
||||
}
|
||||
.onScrollGeometryChange(for: Bool.self) { geometry in
|
||||
geometry.containerSize.height > 0 &&
|
||||
geometry.contentSize.height + geometry.contentInsets.bottom - geometry.visibleRect.maxY <= 24
|
||||
geometry.contentOffset.y + geometry.contentInsets.top <= 24
|
||||
} action: { _, atBottom in
|
||||
#if DEBUG
|
||||
AgentRenderDiagnostics.record("scrollGeometry")
|
||||
@@ -114,7 +114,7 @@ private struct AgentTranscriptView: View {
|
||||
if !atLatest {
|
||||
Button {
|
||||
atLatest = true
|
||||
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.24)) { proxy.scrollTo(Self.bottomID, anchor: .bottom) }
|
||||
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.24)) { proxy.scrollTo(Self.bottomID, anchor: .top) }
|
||||
} label: {
|
||||
Label(room.unreadCount > 0 ? "\(room.unreadCount) unread" : "Latest", systemImage: "arrow.down")
|
||||
.font(.sybil(.caption, weight: .semibold))
|
||||
@@ -130,22 +130,17 @@ private struct AgentTranscriptView: View {
|
||||
appeared = true
|
||||
if !room.isLoading {
|
||||
historyStartID = displayed.first?.id
|
||||
proxy.scrollTo(Self.bottomID, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: room.isLoading) { _, loading in
|
||||
if !loading && historyStartID == nil {
|
||||
historyStartID = displayed.first?.id
|
||||
proxy.scrollTo(Self.bottomID, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
.onChange(of: allMessages.count) { _, _ in
|
||||
if atLatest { proxy.scrollTo(Self.bottomID, anchor: .bottom) }
|
||||
}
|
||||
.onChange(of: latestRequest) { _, _ in
|
||||
if !atLatest {
|
||||
atLatest = true
|
||||
proxy.scrollTo(Self.bottomID, anchor: .bottom)
|
||||
proxy.scrollTo(Self.bottomID, anchor: .top)
|
||||
}
|
||||
}
|
||||
.modifier(AgentReadAcknowledgement(room: room, tracking: scrollTracking,
|
||||
|
||||
Reference in New Issue
Block a user