diff --git a/Apps/MagnumOpus/ViewModels/MailViewModel.swift b/Apps/MagnumOpus/ViewModels/MailViewModel.swift index 5e9f173..b126750 100644 --- a/Apps/MagnumOpus/ViewModels/MailViewModel.swift +++ b/Apps/MagnumOpus/ViewModels/MailViewModel.swift @@ -168,12 +168,12 @@ final class MailViewModel { } } - func startObservingThreads(accountId: String) { + func startObservingThreads(accountId: String, mailboxId: String? = nil) { guard let store else { return } threadObservation?.cancel() threadObservation = Task { do { - for try await summaries in store.observeThreadSummaries(accountId: accountId) { + for try await summaries in store.observeThreadSummaries(accountId: accountId, mailboxId: mailboxId) { self.threads = summaries } } catch { @@ -184,6 +184,22 @@ final class MailViewModel { } } + func selectMailbox(_ mailbox: MailboxInfo) { + selectedMailbox = mailbox + selectedItem = nil + items = [] + guard let accountConfig else { return } + startObservingThreads(accountId: accountConfig.id, mailboxId: mailbox.id) + } + + func selectPerspective(_ perspective: Perspective) { + selectedMailbox = nil + selectedThread = nil + messages = [] + loadPerspective(perspective) + loadAllPerspectiveCounts() + } + func selectThread(_ thread: ThreadSummary) { selectedThread = thread messageObservation?.cancel() @@ -231,6 +247,10 @@ final class MailViewModel { // Reload mailboxes after sync (they may have been created on first sync) if let accountConfig { await loadMailboxes(accountId: accountConfig.id) + // On first sync, default to INBOX view + if selectedMailbox == nil, let inbox = mailboxes.first(where: { $0.name.lowercased() == "inbox" }) { + selectMailbox(inbox) + } } } catch { let desc = "\(error)" diff --git a/Apps/MagnumOpus/Views/SidebarView.swift b/Apps/MagnumOpus/Views/SidebarView.swift index 070f8fb..602c58e 100644 --- a/Apps/MagnumOpus/Views/SidebarView.swift +++ b/Apps/MagnumOpus/Views/SidebarView.swift @@ -9,26 +9,28 @@ struct SidebarView: View { Section("Perspectives") { ForEach(Perspective.allCases) { perspective in Button { - viewModel.loadPerspective(perspective) + viewModel.selectPerspective(perspective) } label: { Label(perspective.label, systemImage: perspective.systemImage) .badge(viewModel.perspectiveCounts[perspective] ?? 0) } .buttonStyle(.plain) - .listItemTint(viewModel.selectedPerspective == perspective ? .accentColor : nil) - .fontWeight(viewModel.selectedPerspective == perspective ? .semibold : .regular) + .listItemTint(viewModel.selectedPerspective == perspective && viewModel.selectedMailbox == nil ? .accentColor : nil) + .fontWeight(viewModel.selectedPerspective == perspective && viewModel.selectedMailbox == nil ? .semibold : .regular) } } Section("Mailboxes") { ForEach(viewModel.mailboxes) { mailbox in Button { - viewModel.selectedMailbox = mailbox + viewModel.selectMailbox(mailbox) } label: { Label(mailbox.name, systemImage: mailbox.systemImage) .badge(mailbox.unreadCount) } .buttonStyle(.plain) + .listItemTint(viewModel.selectedMailbox?.id == mailbox.id ? .accentColor : nil) + .fontWeight(viewModel.selectedMailbox?.id == mailbox.id ? .semibold : .regular) } } } diff --git a/Packages/MagnumOpusCore/Sources/MailStore/Queries.swift b/Packages/MagnumOpusCore/Sources/MailStore/Queries.swift index e4a002e..355a98c 100644 --- a/Packages/MagnumOpusCore/Sources/MailStore/Queries.swift +++ b/Packages/MagnumOpusCore/Sources/MailStore/Queries.swift @@ -16,16 +16,16 @@ extension MailStore { } /// Thread summaries with unread count and latest sender info, ordered by lastDate DESC - public func threadSummaries(accountId: String) throws -> [ThreadSummary] { + public func threadSummaries(accountId: String, mailboxId: String? = nil) throws -> [ThreadSummary] { try dbWriter.read { db in - try Self.threadSummariesFromDB(db, accountId: accountId) + try Self.threadSummariesFromDB(db, accountId: accountId, mailboxId: mailboxId) } } /// Observe thread summaries reactively — UI updates automatically on DB change. - public func observeThreadSummaries(accountId: String) -> AsyncValueObservation<[ThreadSummary]> { + public func observeThreadSummaries(accountId: String, mailboxId: String? = nil) -> AsyncValueObservation<[ThreadSummary]> { ValueObservation.tracking { db -> [ThreadSummary] in - try Self.threadSummariesFromDB(db, accountId: accountId) + try Self.threadSummariesFromDB(db, accountId: accountId, mailboxId: mailboxId) }.values(in: dbWriter) } @@ -44,7 +44,19 @@ extension MailStore { // MARK: - Internal helpers - static func threadSummariesFromDB(_ db: Database, accountId: String) throws -> [ThreadSummary] { + static func threadSummariesFromDB(_ db: Database, accountId: String, mailboxId: String? = nil) throws -> [ThreadSummary] { + let mailboxFilter: String + let arguments: StatementArguments + + if let mailboxId { + // Filter threads to only those containing messages in the specified mailbox + mailboxFilter = "AND EXISTS (SELECT 1 FROM threadMessage tm2 JOIN message m2 ON m2.id = tm2.messageId WHERE tm2.threadId = t.id AND m2.mailboxId = ?)" + arguments = [accountId, mailboxId] + } else { + mailboxFilter = "" + arguments = [accountId] + } + let sql = """ SELECT t.id, t.accountId, t.subject, t.lastDate, t.messageCount, @@ -53,10 +65,10 @@ extension MailStore { (SELECT m.snippet FROM threadMessage tm JOIN message m ON m.id = tm.messageId WHERE tm.threadId = t.id ORDER BY m.date DESC LIMIT 1) as snippet, (SELECT COUNT(*) FROM attachment a JOIN threadMessage tm ON tm.messageId = a.messageId WHERE tm.threadId = t.id) as attachmentCount FROM thread t - WHERE t.accountId = ? + WHERE t.accountId = ? \(mailboxFilter) ORDER BY t.lastDate DESC """ - let rows = try Row.fetchAll(db, sql: sql, arguments: [accountId]) + let rows = try Row.fetchAll(db, sql: sql, arguments: arguments) return rows.map { row in ThreadSummary( id: row["id"],