Implements attachment previewing
This commit is contained in:
152
kordophone2/Transcript/TranscriptView.swift
Normal file
152
kordophone2/Transcript/TranscriptView.swift
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// TranscriptView.swift
|
||||
// kordophone2
|
||||
//
|
||||
// Created by James Magahern on 8/24/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TranscriptView: View
|
||||
{
|
||||
@Binding var model: ViewModel
|
||||
|
||||
@Environment(\.xpcClient) private var xpcClient
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 17.0) {
|
||||
ForEach($model.displayItems.reversed()) { item in
|
||||
displayItemView(item.wrappedValue)
|
||||
.id(item.id)
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
.transition(
|
||||
.push(from: .bottom)
|
||||
.combined(with: .opacity)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.scaleEffect(CGSize(width: 1.0, height: -1.0))
|
||||
.task { await watchForMessageListChanges() }
|
||||
}
|
||||
|
||||
private func watchForMessageListChanges() async {
|
||||
for await event in xpcClient.eventStream() {
|
||||
switch event {
|
||||
case .attachmentDownloaded(let attachmentId):
|
||||
model.attachmentDownloaded(id: attachmentId)
|
||||
case .messagesUpdated(let conversationId):
|
||||
if conversationId == model.displayedConversation {
|
||||
model.setNeedsReload(animated: true)
|
||||
}
|
||||
case .updateStreamReconnected:
|
||||
model.setNeedsReload(animated: false)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func displayItemView(_ item: DisplayItem) -> some View {
|
||||
switch item {
|
||||
case .message(let message):
|
||||
TextBubbleItemView(text: message.text, isFromMe: message.isFromMe)
|
||||
case .date(let date):
|
||||
DateItemView(date: date)
|
||||
case .attachment(let attachment):
|
||||
if attachment.isPreviewDownloaded {
|
||||
ImageItemView(
|
||||
isFromMe: attachment.sender.isMe,
|
||||
attachment: attachment
|
||||
)
|
||||
} else {
|
||||
PlaceholderImageItemView(
|
||||
isFromMe: attachment.sender.isMe,
|
||||
size: attachment.size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
@Observable
|
||||
class ViewModel
|
||||
{
|
||||
var displayItems: [DisplayItem] = []
|
||||
var displayedConversation: Display.Conversation.ID? = nil
|
||||
|
||||
internal var needsReload: NeedsReload = .yes(false)
|
||||
internal var messages: [Display.Message]
|
||||
|
||||
init(messages: [Display.Message] = []) {
|
||||
self.messages = messages
|
||||
observeDisplayedConversation()
|
||||
rebuildDisplayItems()
|
||||
}
|
||||
|
||||
func setNeedsReload(animated: Bool) {
|
||||
needsReload = .yes(animated)
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
await reloadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
func attachmentDownloaded(id: String) {
|
||||
// TODO: should be smarter here
|
||||
setNeedsReload(animated: true)
|
||||
}
|
||||
|
||||
private func observeDisplayedConversation() {
|
||||
withObservationTracking {
|
||||
_ = displayedConversation
|
||||
} onChange: {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
setNeedsReload(animated: false)
|
||||
observeDisplayedConversation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadMessages() async {
|
||||
guard case .yes(let animated) = needsReload else { return }
|
||||
needsReload = .no
|
||||
|
||||
guard let displayedConversation else { return }
|
||||
|
||||
do {
|
||||
let client = XPCClient()
|
||||
let clientMessages = try await client.getMessages(conversationId: displayedConversation)
|
||||
.map { Display.Message(from: $0) }
|
||||
|
||||
self.messages = clientMessages
|
||||
self.rebuildDisplayItems(animated: animated)
|
||||
} catch {
|
||||
print("Message fetch error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum NeedsReload
|
||||
{
|
||||
case no
|
||||
case yes(Bool) // animated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@Previewable @State var model = TranscriptView.ViewModel(messages: [
|
||||
.init(sender: .me, text: "Hello, how are you?"),
|
||||
.init(sender: .counterpart("Bob"), text: "I am doing fine!")
|
||||
])
|
||||
|
||||
TranscriptView(model: $model)
|
||||
}
|
||||
Reference in New Issue
Block a user