osx: better scroll view management
This commit is contained in:
@@ -14,7 +14,7 @@ struct ConversationView: View
|
||||
@Binding var entryModel: MessageEntryView.ViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(spacing: 0.0) {
|
||||
TranscriptView(model: $transcriptModel)
|
||||
MessageEntryView(viewModel: $entryModel)
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
|
||||
extension TranscriptView.ViewModel
|
||||
{
|
||||
internal func rebuildDisplayItems(animated: Bool = false) {
|
||||
internal func rebuildDisplayItems(animated: Bool = false, completion: () -> Void = {}) {
|
||||
var displayItems: [DisplayItem] = []
|
||||
var lastDate: Date = .distantPast
|
||||
var lastSender: Display.Sender? = nil
|
||||
@@ -53,6 +53,7 @@ extension TranscriptView.ViewModel
|
||||
let animation: Animation? = animated ? .default : nil
|
||||
withAnimation(animation) {
|
||||
self.displayItems = displayItems
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,56 @@ struct TranscriptView: View
|
||||
|
||||
@Environment(\.xpcClient) private var xpcClient
|
||||
|
||||
init(model: Binding<ViewModel>) {
|
||||
self._model = model
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 6.0) {
|
||||
ForEach($model.displayItems.reversed()) { item in
|
||||
displayItemView(item.wrappedValue)
|
||||
.id(item.id)
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
.transition(
|
||||
.push(from: .top)
|
||||
.combined(with: .opacity)
|
||||
)
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
// For resetting scroll position to the "bottom"
|
||||
EmptyView()
|
||||
.id(ViewID.bottomAnchor)
|
||||
|
||||
LazyVStack(spacing: 6.0) {
|
||||
ForEach($model.displayItems.reversed()) { item in
|
||||
displayItemView(item.wrappedValue)
|
||||
.id(item.id)
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
.transition(
|
||||
.push(from: .top)
|
||||
.combined(with: .opacity)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// Flip vertically so newest messages are at the bottom.
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
|
||||
// Watch for xpc events
|
||||
.task { await watchForMessageListChanges() }
|
||||
|
||||
// On conversation change, reload displayed messages and mark as read.
|
||||
.onChange(of: model.displayedConversation) { oldValue, newValue in
|
||||
Task {
|
||||
guard oldValue != newValue else { return }
|
||||
|
||||
// Reload NOW
|
||||
await model.reloadMessages(animated: false) {
|
||||
// Once that's done, scroll to the "bottom" (actually top)
|
||||
proxy.scrollTo(ViewID.bottomAnchor, anchor: .top)
|
||||
}
|
||||
}
|
||||
|
||||
Task.detached {
|
||||
// Mark as read on server, and trigger a sync.
|
||||
await model.markAsRead()
|
||||
await model.triggerSync()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
.id(model.displayedConversation?.id)
|
||||
.task { await watchForMessageListChanges() }
|
||||
}
|
||||
|
||||
private func watchForMessageListChanges() async {
|
||||
@@ -82,6 +114,13 @@ struct TranscriptView: View
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum ViewID: String
|
||||
{
|
||||
case bottomAnchor
|
||||
}
|
||||
|
||||
// MARK: - View Model
|
||||
|
||||
@Observable
|
||||
class ViewModel
|
||||
{
|
||||
@@ -94,7 +133,6 @@ struct TranscriptView: View
|
||||
|
||||
init(messages: [Display.Message] = []) {
|
||||
self.messages = messages
|
||||
observeDisplayedConversation()
|
||||
rebuildDisplayItems()
|
||||
}
|
||||
|
||||
@@ -106,7 +144,7 @@ struct TranscriptView: View
|
||||
needsReload = .yes(animated)
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
await reloadMessages()
|
||||
await reloadIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,22 +153,6 @@ struct TranscriptView: View
|
||||
setNeedsReload(animated: false)
|
||||
}
|
||||
|
||||
private func observeDisplayedConversation() {
|
||||
withObservationTracking {
|
||||
_ = displayedConversation
|
||||
} onChange: {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
await markAsRead()
|
||||
await triggerSync()
|
||||
|
||||
setNeedsReload(animated: false)
|
||||
observeDisplayedConversation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markAsRead() async {
|
||||
guard let displayedConversation else { return }
|
||||
|
||||
@@ -151,10 +173,14 @@ struct TranscriptView: View
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadMessages() async {
|
||||
func reloadIfNeeded(completion: () -> Void = {}) async {
|
||||
guard case .yes(let animated) = needsReload else { return }
|
||||
needsReload = .no
|
||||
|
||||
await reloadMessages(animated: animated, completion: completion)
|
||||
}
|
||||
|
||||
func reloadMessages(animated: Bool, completion: () -> Void) async {
|
||||
guard let displayedConversation else { return }
|
||||
|
||||
do {
|
||||
@@ -167,8 +193,10 @@ struct TranscriptView: View
|
||||
// Only animate for incoming messages.
|
||||
let shouldAnimate = (newIds.count == 1)
|
||||
|
||||
self.messages = clientMessages
|
||||
self.rebuildDisplayItems(animated: animated && shouldAnimate)
|
||||
await MainActor.run {
|
||||
self.messages = clientMessages
|
||||
self.rebuildDisplayItems(animated: animated && shouldAnimate, completion: completion)
|
||||
}
|
||||
} catch {
|
||||
print("Message fetch error: \(error)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user