ios: experiment with inverted Agent transcript layout
TestFlight / Build and upload (push) Successful in 1m58s
TestFlight / Build and upload (push) Successful in 1m58s
This commit is contained in:
@@ -45,25 +45,9 @@ private struct AgentTranscriptView: View {
|
|||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading, spacing: 15) {
|
LazyVStack(alignment: .leading, spacing: 15) {
|
||||||
if allMessages.count > displayed.count {
|
// The native scroll origin is the visual bottom. Keep model
|
||||||
Button("Load earlier messages") {
|
// and cursor order ascending; reverse only presentation.
|
||||||
// Loading older history may move the viewport as lazy
|
Color.clear.frame(height: 17).id(Self.bottomID)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
if rendered.isTyping {
|
if rendered.isTyping {
|
||||||
HStack(spacing: 9) {
|
HStack(spacing: 9) {
|
||||||
AgentAvatar()
|
AgentAvatar()
|
||||||
@@ -72,33 +56,49 @@ private struct AgentTranscriptView: View {
|
|||||||
}
|
}
|
||||||
.foregroundStyle(SybilTheme.textMuted)
|
.foregroundStyle(SybilTheme.textMuted)
|
||||||
.accessibilityIdentifier("agent-typing")
|
.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.
|
// Keep native lazy-row targets without binding scroll position.
|
||||||
.scrollTargetLayout()
|
.scrollTargetLayout()
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
// Match normal chat: content clears the header, while the
|
// Match normal chat: content clears the header, while the
|
||||||
// viewport extends beneath the shared navigation fade.
|
// viewport extends beneath the shared navigation fade.
|
||||||
.padding(.top, 16 + topInset)
|
.padding(.bottom, 16 + topInset)
|
||||||
.frame(maxWidth: SybilLayout.webContentMaxWidth)
|
.frame(maxWidth: SybilLayout.webContentMaxWidth)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
.scaleEffect(x: 1, y: -1)
|
||||||
.accessibilityIdentifier("agent-transcript")
|
.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)
|
.scrollDismissesKeyboard(.interactively)
|
||||||
.onScrollPhaseChange { _, phase in
|
.onScrollPhaseChange { _, phase in
|
||||||
scrollPhase = phase
|
scrollPhase = phase
|
||||||
}
|
}
|
||||||
.onScrollGeometryChange(for: Bool.self) { geometry in
|
.onScrollGeometryChange(for: Bool.self) { geometry in
|
||||||
geometry.containerSize.height > 0 &&
|
geometry.containerSize.height > 0 &&
|
||||||
geometry.contentSize.height + geometry.contentInsets.bottom - geometry.visibleRect.maxY <= 24
|
geometry.contentOffset.y + geometry.contentInsets.top <= 24
|
||||||
} action: { _, atBottom in
|
} action: { _, atBottom in
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
AgentRenderDiagnostics.record("scrollGeometry")
|
AgentRenderDiagnostics.record("scrollGeometry")
|
||||||
@@ -114,7 +114,7 @@ private struct AgentTranscriptView: View {
|
|||||||
if !atLatest {
|
if !atLatest {
|
||||||
Button {
|
Button {
|
||||||
atLatest = true
|
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: {
|
||||||
Label(room.unreadCount > 0 ? "\(room.unreadCount) unread" : "Latest", systemImage: "arrow.down")
|
Label(room.unreadCount > 0 ? "\(room.unreadCount) unread" : "Latest", systemImage: "arrow.down")
|
||||||
.font(.sybil(.caption, weight: .semibold))
|
.font(.sybil(.caption, weight: .semibold))
|
||||||
@@ -130,22 +130,17 @@ private struct AgentTranscriptView: View {
|
|||||||
appeared = true
|
appeared = true
|
||||||
if !room.isLoading {
|
if !room.isLoading {
|
||||||
historyStartID = displayed.first?.id
|
historyStartID = displayed.first?.id
|
||||||
proxy.scrollTo(Self.bottomID, anchor: .bottom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: room.isLoading) { _, loading in
|
.onChange(of: room.isLoading) { _, loading in
|
||||||
if !loading && historyStartID == nil {
|
if !loading && historyStartID == nil {
|
||||||
historyStartID = displayed.first?.id
|
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
|
.onChange(of: latestRequest) { _, _ in
|
||||||
if !atLatest {
|
if !atLatest {
|
||||||
atLatest = true
|
atLatest = true
|
||||||
proxy.scrollTo(Self.bottomID, anchor: .bottom)
|
proxy.scrollTo(Self.bottomID, anchor: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(AgentReadAcknowledgement(room: room, tracking: scrollTracking,
|
.modifier(AgentReadAcknowledgement(room: room, tracking: scrollTracking,
|
||||||
|
|||||||
Reference in New Issue
Block a user