From 0b94d5b3fa23b24329ae5d4d90feba2bd9cc7038 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 3 May 2026 21:21:03 -0700 Subject: [PATCH] ios: invert scroll view technique --- .../Sybil/SybilChatTranscriptView.swift | 83 +++++++------------ 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift index 728265b..85545e4 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift @@ -7,7 +7,6 @@ struct SybilChatTranscriptView: View { var isSending: Bool var topContentInset: CGFloat = 0 var bottomContentInset: CGFloat = 0 - @State private var hasHandledInitialTranscriptScroll = false private var hasPendingAssistant: Bool { messages.contains { message in @@ -16,66 +15,42 @@ struct SybilChatTranscriptView: View { } var body: some View { - ScrollViewReader { proxy in - ScrollView { - LazyVStack(alignment: .leading, spacing: 26) { - if isLoading && messages.isEmpty { - Text("Loading messages…") + ScrollView { + LazyVStack(alignment: .leading, spacing: 26) { + if isSending && !hasPendingAssistant { + HStack(spacing: 8) { + ProgressView() + .controlSize(.small) + .tint(SybilTheme.textMuted) + Text("Assistant is typing…") .font(.sybil(.footnote)) .foregroundStyle(SybilTheme.textMuted) - .padding(.top, 24) } - - ForEach(messages) { message in - MessageBubble(message: message, isSending: isSending) - .frame(maxWidth: .infinity) - .id(message.id) - } - - if isSending && !hasPendingAssistant { - HStack(spacing: 8) { - ProgressView() - .controlSize(.small) - .tint(SybilTheme.textMuted) - Text("Assistant is typing…") - .font(.sybil(.footnote)) - .foregroundStyle(SybilTheme.textMuted) - } - .id("typing-indicator") - } - - Color.clear - .frame(height: 2) - .id("chat-bottom-anchor") + .scaleEffect(x: 1, y: -1) + } + + ForEach(messages.reversed()) { message in + MessageBubble(message: message, isSending: isSending) + .frame(maxWidth: .infinity) + .scaleEffect(x: 1, y: -1) + } + + if isLoading && messages.isEmpty { + Text("Loading messages…") + .font(.sybil(.footnote)) + .foregroundStyle(SybilTheme.textMuted) + .padding(.top, 24) + .scaleEffect(x: 1, y: -1) } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 14) - .padding(.top, 18 + topContentInset) - .padding(.bottom, 18 + bottomContentInset) } .frame(maxWidth: .infinity, alignment: .leading) - .scrollDismissesKeyboard(.interactively) - .onAppear { - scrollToBottom(with: proxy, animated: false) - } - .onChange(of: messages.map(\.id)) { _, _ in - scrollToBottom(with: proxy, animated: hasHandledInitialTranscriptScroll && !isLoading) - hasHandledInitialTranscriptScroll = true - } - .onChange(of: isSending) { _, _ in - scrollToBottom(with: proxy, animated: hasHandledInitialTranscriptScroll) - } - } - } - - private func scrollToBottom(with proxy: ScrollViewProxy, animated: Bool) { - if animated { - withAnimation(.easeOut(duration: 0.22)) { - proxy.scrollTo("chat-bottom-anchor", anchor: .bottom) - } - } else { - proxy.scrollTo("chat-bottom-anchor", anchor: .bottom) + .padding(.horizontal, 14) + .padding(.top, 18 + bottomContentInset) + .padding(.bottom, 18 + topContentInset) } + .frame(maxWidth: .infinity, alignment: .leading) + .scrollDismissesKeyboard(.interactively) + .scaleEffect(x: 1, y: -1) } }