osx: better scroll view management
This commit is contained in:
@@ -14,7 +14,7 @@ struct ConversationView: View
|
|||||||
@Binding var entryModel: MessageEntryView.ViewModel
|
@Binding var entryModel: MessageEntryView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(spacing: 0.0) {
|
||||||
TranscriptView(model: $transcriptModel)
|
TranscriptView(model: $transcriptModel)
|
||||||
MessageEntryView(viewModel: $entryModel)
|
MessageEntryView(viewModel: $entryModel)
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
|
|
||||||
extension TranscriptView.ViewModel
|
extension TranscriptView.ViewModel
|
||||||
{
|
{
|
||||||
internal func rebuildDisplayItems(animated: Bool = false) {
|
internal func rebuildDisplayItems(animated: Bool = false, completion: () -> Void = {}) {
|
||||||
var displayItems: [DisplayItem] = []
|
var displayItems: [DisplayItem] = []
|
||||||
var lastDate: Date = .distantPast
|
var lastDate: Date = .distantPast
|
||||||
var lastSender: Display.Sender? = nil
|
var lastSender: Display.Sender? = nil
|
||||||
@@ -53,6 +53,7 @@ extension TranscriptView.ViewModel
|
|||||||
let animation: Animation? = animated ? .default : nil
|
let animation: Animation? = animated ? .default : nil
|
||||||
withAnimation(animation) {
|
withAnimation(animation) {
|
||||||
self.displayItems = displayItems
|
self.displayItems = displayItems
|
||||||
|
completion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,25 +12,57 @@ struct TranscriptView: View
|
|||||||
@Binding var model: ViewModel
|
@Binding var model: ViewModel
|
||||||
|
|
||||||
@Environment(\.xpcClient) private var xpcClient
|
@Environment(\.xpcClient) private var xpcClient
|
||||||
|
|
||||||
|
init(model: Binding<ViewModel>) {
|
||||||
|
self._model = model
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollViewReader { proxy in
|
||||||
LazyVStack(spacing: 6.0) {
|
ScrollView {
|
||||||
ForEach($model.displayItems.reversed()) { item in
|
// For resetting scroll position to the "bottom"
|
||||||
displayItemView(item.wrappedValue)
|
EmptyView()
|
||||||
.id(item.id)
|
.id(ViewID.bottomAnchor)
|
||||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
|
||||||
.transition(
|
LazyVStack(spacing: 6.0) {
|
||||||
.push(from: .top)
|
ForEach($model.displayItems.reversed()) { item in
|
||||||
.combined(with: .opacity)
|
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 {
|
private func watchForMessageListChanges() async {
|
||||||
@@ -79,9 +111,16 @@ struct TranscriptView: View
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Types
|
// MARK: - Types
|
||||||
|
|
||||||
|
enum ViewID: String
|
||||||
|
{
|
||||||
|
case bottomAnchor
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - View Model
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class ViewModel
|
class ViewModel
|
||||||
{
|
{
|
||||||
@@ -94,7 +133,6 @@ struct TranscriptView: View
|
|||||||
|
|
||||||
init(messages: [Display.Message] = []) {
|
init(messages: [Display.Message] = []) {
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
observeDisplayedConversation()
|
|
||||||
rebuildDisplayItems()
|
rebuildDisplayItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +144,7 @@ struct TranscriptView: View
|
|||||||
needsReload = .yes(animated)
|
needsReload = .yes(animated)
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
await reloadMessages()
|
await reloadIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,22 +153,6 @@ struct TranscriptView: View
|
|||||||
setNeedsReload(animated: false)
|
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 {
|
func markAsRead() async {
|
||||||
guard let displayedConversation else { return }
|
guard let displayedConversation else { return }
|
||||||
|
|
||||||
@@ -150,11 +172,15 @@ struct TranscriptView: View
|
|||||||
print("Error triggering sync: \(error)")
|
print("Error triggering sync: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reloadMessages() async {
|
func reloadIfNeeded(completion: () -> Void = {}) async {
|
||||||
guard case .yes(let animated) = needsReload else { return }
|
guard case .yes(let animated) = needsReload else { return }
|
||||||
needsReload = .no
|
needsReload = .no
|
||||||
|
|
||||||
|
await reloadMessages(animated: animated, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadMessages(animated: Bool, completion: () -> Void) async {
|
||||||
guard let displayedConversation else { return }
|
guard let displayedConversation else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -167,8 +193,10 @@ struct TranscriptView: View
|
|||||||
// Only animate for incoming messages.
|
// Only animate for incoming messages.
|
||||||
let shouldAnimate = (newIds.count == 1)
|
let shouldAnimate = (newIds.count == 1)
|
||||||
|
|
||||||
self.messages = clientMessages
|
await MainActor.run {
|
||||||
self.rebuildDisplayItems(animated: animated && shouldAnimate)
|
self.messages = clientMessages
|
||||||
|
self.rebuildDisplayItems(animated: animated && shouldAnimate, completion: completion)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Message fetch error: \(error)")
|
print("Message fetch error: \(error)")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user