import SwiftUI struct UnreadListView: View { @Environment(APIClient.self) private var api @Environment(MeshDataStore.self) private var store @Environment(ReadTracker.self) private var readTracker @State private var entries: [UnreadEntry] = [] @State private var loading = false @State private var error: String? var body: some View { Group { if loading && entries.isEmpty { ProgressView("Checking unread…") } else if let error, entries.isEmpty { ContentUnavailableView { Label("Error", systemImage: "exclamationmark.triangle") } description: { Text(error) } actions: { Button("Retry") { Task { await load() } } } } else if entries.isEmpty { ContentUnavailableView("All caught up", systemImage: "checkmark.circle") } else { List(entries) { entry in NavigationLink { ChatView(target: .dm(nodeId: entry.nodeId, name: entry.displayName)) } label: { UnreadRow(entry: entry) } } } } .navigationTitle("Unread") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { Task { await load() } } label: { Image(systemName: "arrow.clockwise") } .disabled(loading) } } .refreshable { await load() } .task { await load() } } @MainActor private func load() async { loading = true defer { loading = false } error = nil if store.nodes.isEmpty || store.localNodeId == nil { await store.refresh(api: api) } do { let msgs = try await api.fetchAllMessages(limit: 200) entries = Self.compute( from: msgs, localNodeId: store.localNodeId, nodeNames: store.nodeNames, readTracker: readTracker ) } catch { self.error = error.localizedDescription } } static func compute(from messages: [Message], localNodeId: String?, nodeNames: [String: String], readTracker: ReadTracker) -> [UnreadEntry] { var byNode: [String: (count: Int, last: Message)] = [:] for msg in messages where msg.channel == -1 && msg.isText && !msg.isReaction { guard let from = msg.fromNodeId else { continue } if from == localNodeId { continue } let lastRead = readTracker.lastRead(for: from) guard msg.timestamp.timeIntervalSince1970 > lastRead else { continue } if var existing = byNode[from] { existing.count += 1 if msg.timestamp > existing.last.timestamp { existing.last = msg } byNode[from] = existing } else { byNode[from] = (1, msg) } } return byNode.map { (id, v) in UnreadEntry( nodeId: id, displayName: nodeNames[id] ?? id, count: v.count, lastMessage: v.last.text ?? "", lastTimestamp: v.last.timestamp ) }.sorted { $0.count > $1.count } } } struct UnreadEntry: Identifiable, Hashable { let nodeId: String let displayName: String let count: Int let lastMessage: String let lastTimestamp: Date var id: String { nodeId } } private struct UnreadRow: View { let entry: UnreadEntry var body: some View { HStack(spacing: 10) { ZStack { Circle().fill(Color.accentColor).frame(width: 32, height: 32) Text("\(entry.count)") .font(.caption.weight(.bold)) .foregroundStyle(.white) } VStack(alignment: .leading, spacing: 2) { Text(entry.displayName).font(.body) Text(entry.lastMessage) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } .padding(.vertical, 2) } }