filter thread list by selected mailbox, fix sidebar selection
- threadSummariesFromDB accepts optional mailboxId filter, uses EXISTS subquery to show only threads containing messages in the selected mailbox - add selectMailbox() and selectPerspective() to MailViewModel for clean state transitions between folder view and GTD perspective view - sidebar highlights selected mailbox/perspective, clears state on switch - default to INBOX mailbox after first sync Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -168,12 +168,12 @@ final class MailViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startObservingThreads(accountId: String) {
|
func startObservingThreads(accountId: String, mailboxId: String? = nil) {
|
||||||
guard let store else { return }
|
guard let store else { return }
|
||||||
threadObservation?.cancel()
|
threadObservation?.cancel()
|
||||||
threadObservation = Task {
|
threadObservation = Task {
|
||||||
do {
|
do {
|
||||||
for try await summaries in store.observeThreadSummaries(accountId: accountId) {
|
for try await summaries in store.observeThreadSummaries(accountId: accountId, mailboxId: mailboxId) {
|
||||||
self.threads = summaries
|
self.threads = summaries
|
||||||
}
|
}
|
||||||
} catch {
|
} 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) {
|
func selectThread(_ thread: ThreadSummary) {
|
||||||
selectedThread = thread
|
selectedThread = thread
|
||||||
messageObservation?.cancel()
|
messageObservation?.cancel()
|
||||||
@@ -231,6 +247,10 @@ final class MailViewModel {
|
|||||||
// Reload mailboxes after sync (they may have been created on first sync)
|
// Reload mailboxes after sync (they may have been created on first sync)
|
||||||
if let accountConfig {
|
if let accountConfig {
|
||||||
await loadMailboxes(accountId: accountConfig.id)
|
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 {
|
} catch {
|
||||||
let desc = "\(error)"
|
let desc = "\(error)"
|
||||||
|
|||||||
@@ -9,26 +9,28 @@ struct SidebarView: View {
|
|||||||
Section("Perspectives") {
|
Section("Perspectives") {
|
||||||
ForEach(Perspective.allCases) { perspective in
|
ForEach(Perspective.allCases) { perspective in
|
||||||
Button {
|
Button {
|
||||||
viewModel.loadPerspective(perspective)
|
viewModel.selectPerspective(perspective)
|
||||||
} label: {
|
} label: {
|
||||||
Label(perspective.label, systemImage: perspective.systemImage)
|
Label(perspective.label, systemImage: perspective.systemImage)
|
||||||
.badge(viewModel.perspectiveCounts[perspective] ?? 0)
|
.badge(viewModel.perspectiveCounts[perspective] ?? 0)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.listItemTint(viewModel.selectedPerspective == perspective ? .accentColor : nil)
|
.listItemTint(viewModel.selectedPerspective == perspective && viewModel.selectedMailbox == nil ? .accentColor : nil)
|
||||||
.fontWeight(viewModel.selectedPerspective == perspective ? .semibold : .regular)
|
.fontWeight(viewModel.selectedPerspective == perspective && viewModel.selectedMailbox == nil ? .semibold : .regular)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Mailboxes") {
|
Section("Mailboxes") {
|
||||||
ForEach(viewModel.mailboxes) { mailbox in
|
ForEach(viewModel.mailboxes) { mailbox in
|
||||||
Button {
|
Button {
|
||||||
viewModel.selectedMailbox = mailbox
|
viewModel.selectMailbox(mailbox)
|
||||||
} label: {
|
} label: {
|
||||||
Label(mailbox.name, systemImage: mailbox.systemImage)
|
Label(mailbox.name, systemImage: mailbox.systemImage)
|
||||||
.badge(mailbox.unreadCount)
|
.badge(mailbox.unreadCount)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.listItemTint(viewModel.selectedMailbox?.id == mailbox.id ? .accentColor : nil)
|
||||||
|
.fontWeight(viewModel.selectedMailbox?.id == mailbox.id ? .semibold : .regular)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ extension MailStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Thread summaries with unread count and latest sender info, ordered by lastDate DESC
|
/// 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 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.
|
/// 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
|
ValueObservation.tracking { db -> [ThreadSummary] in
|
||||||
try Self.threadSummariesFromDB(db, accountId: accountId)
|
try Self.threadSummariesFromDB(db, accountId: accountId, mailboxId: mailboxId)
|
||||||
}.values(in: dbWriter)
|
}.values(in: dbWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,19 @@ extension MailStore {
|
|||||||
|
|
||||||
// MARK: - Internal helpers
|
// 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 = """
|
let sql = """
|
||||||
SELECT
|
SELECT
|
||||||
t.id, t.accountId, t.subject, t.lastDate, t.messageCount,
|
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 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
|
(SELECT COUNT(*) FROM attachment a JOIN threadMessage tm ON tm.messageId = a.messageId WHERE tm.threadId = t.id) as attachmentCount
|
||||||
FROM thread t
|
FROM thread t
|
||||||
WHERE t.accountId = ?
|
WHERE t.accountId = ? \(mailboxFilter)
|
||||||
ORDER BY t.lastDate DESC
|
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
|
return rows.map { row in
|
||||||
ThreadSummary(
|
ThreadSummary(
|
||||||
id: row["id"],
|
id: row["id"],
|
||||||
|
|||||||
Reference in New Issue
Block a user