// // ConversationListView.swift // kordophone2 // // Created by James Magahern on 8/24/25. // import SwiftUI struct ConversationListView: View { @Binding var model: ViewModel @Environment(\.xpcClient) private var xpcClient var body: some View { List($model.conversations, selection: $model.selectedConversation) { conv in let isUnread = conv.wrappedValue.unreadCount > 0 HStack(spacing: 0.0) { if isUnread { Image(systemName: "circlebadge.fill") .foregroundStyle(.tint) .frame(width: 10.0) } else { Rectangle() .foregroundStyle(.clear) .frame(width: 10.0) } VStack(alignment: .leading) { Text(conv.wrappedValue.displayName) .bold() Text(conv.wrappedValue.messagePreview) .foregroundStyle(.secondary) } .padding(8.0) } .id(conv.id) } .listStyle(.sidebar) .task { await watchForConversationListChanges() } .task { await model.triggerSync() } } private func watchForConversationListChanges() async { for await event in xpcClient.eventStream() { switch event { case .conversationsUpdated: model.setNeedsReload() case .messagesUpdated(_): await model.triggerSync() case .updateStreamReconnected: await model.triggerSync() default: break } } } // MARK: - Types @Observable class ViewModel { var conversations: [Display.Conversation] var selectedConversation: Display.Conversation.ID? private var needsReload: Bool = true private let client = XPCClient() public init(conversations: [Display.Conversation] = []) { self.conversations = conversations self.selectedConversation = nil setNeedsReload() } func triggerSync() async { do { try await client.syncConversationList() } catch { print("Conversation List Sync Error: \(error)") } } func setNeedsReload() { needsReload = true Task { @MainActor [weak self] in guard let self else { return } await reloadConversations() } } func reloadConversations() async { guard needsReload else { return } needsReload = false do { let clientConversations = try await client.getConversations() .map { Display.Conversation(from: $0) } self.conversations = clientConversations let unreadConversations = clientConversations.filter(\.isUnread) await MainActor.run { NSApplication.shared.dockTile.badgeLabel = unreadConversations.isEmpty ? nil : "\(unreadConversations.count)" } } catch { print("Error reloading conversations: \(error)") } } } } #Preview { @Previewable @State var viewModel = ConversationListView.ViewModel(conversations: [ .init(id: "asdf", name: "Cool", participants: ["me"], messagePreview: "Hello there"), .init(id: "gjkl", name: "Nice", participants: ["me"], messagePreview: "How are you"), ]) ConversationListView(model: $viewModel) }