import Foundation import Observation import SwiftUI @MainActor @Observable final class WatchInboxStore { var threads: [MailThread] = [] var selectedThreadID: MailThread.ID? private let service = MockMailService() var visibleThreads: [MailThread] { Array( threads .filter { $0.mailbox == .inbox } .sorted { $0.lastUpdated > $1.lastUpdated } .prefix(4) ) } var selectedThread: MailThread? { visibleThreads.first(where: { $0.id == selectedThreadID }) ?? visibleThreads.first } func load() async { guard threads.isEmpty else { return } threads = (try? await service.loadThreads()) ?? service.previewThreads() selectedThreadID = visibleThreads.first?.id } func select(_ thread: MailThread) { selectedThreadID = thread.id } func archive(_ thread: MailThread) { guard let index = threads.firstIndex(where: { $0.id == thread.id }) else { return } threads[index].mailbox = .archive if selectedThreadID == thread.id { selectedThreadID = visibleThreads.first?.id } } } struct WatchInboxView: View { @Bindable var store: WatchInboxStore var body: some View { List(store.visibleThreads) { thread in NavigationLink { WatchThreadView(thread: thread, store: store) } label: { WatchInboxRow( thread: thread, isHighlighted: thread.id == (store.selectedThread?.id ?? store.visibleThreads.first?.id) ) } .buttonStyle(.plain) .simultaneousGesture(TapGesture().onEnded { store.select(thread) }) } .listStyle(.carousel) .navigationTitle("Inbox") .task { await store.load() } } } private struct WatchInboxRow: View { let thread: MailThread let isHighlighted: Bool var body: some View { HStack(spacing: 10) { AvatarCircle(name: senderName, color: thread.lane.color) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(senderName) .font(.headline) .lineLimit(1) if thread.isUnread { Circle() .fill(SIO.tint) .frame(width: 6, height: 6) } } Text(thread.subject) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } .padding(.vertical, 4) .padding(.horizontal, 6) .background(isHighlighted ? SIO.tint.opacity(0.12) : Color.clear, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) } private var senderName: String { thread.latestMessage?.sender.name ?? thread.participants.first?.name ?? "Unknown" } } struct WatchThreadView: View { let thread: MailThread @Bindable var store: WatchInboxStore @Environment(\.openURL) private var openURL var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 10) { AvatarCircle(name: senderName, color: thread.lane.color) VStack(alignment: .leading, spacing: 2) { Text(senderName) .font(.headline) Text(thread.lane.label) .font(.caption2) .foregroundStyle(thread.lane.color) } } Text(thread.subject) .font(.headline) .lineLimit(2) Text(thread.previewText) .font(.caption) .foregroundStyle(.secondary) .lineLimit(4) Button("Reply") { openURL(URL(string: "socialio://compose?to=\(replyTarget)&subject=Re:%20\(thread.subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? thread.subject)")!) } .buttonStyle(.borderedProminent) .tint(SIO.tint) Button { store.archive(thread) } label: { Image(systemName: "archivebox") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } .padding(.horizontal, 6) } } private var senderName: String { thread.latestMessage?.sender.name ?? thread.participants.first?.name ?? "Unknown" } private var replyTarget: String { (thread.latestMessage?.sender.email ?? thread.participants.first?.email ?? "hello@social.io") .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "hello@social.io" } } private struct AvatarCircle: View { let name: String let color: Color var body: some View { Text(initials) .font(.system(size: 9, weight: .semibold, design: .rounded)) .foregroundStyle(color) .frame(width: 22, height: 22) .background(color.opacity(0.14), in: Circle()) } private var initials: String { String(name.split(separator: " ").prefix(2).compactMap { $0.first }) .uppercased() } } #Preview("Watch Inbox") { NavigationStack { WatchInboxView(store: previewStore()) } } #Preview("Watch Thread") { NavigationStack { if let thread = previewStore().visibleThreads.first { WatchThreadView(thread: thread, store: previewStore()) } } } @MainActor private func previewStore() -> WatchInboxStore { let store = WatchInboxStore() store.threads = MockMailService().previewThreads() store.selectedThreadID = store.visibleThreads.first?.id return store }