fix code review issues: deferral date format, storeFlags SELECT, event loop leaks, GTD selection tracking

- fix deferral resurfacing using VTODOParser.formatDateOnly instead of ISO8601 (date format mismatch)
- add SELECT mailbox before UID STORE in storeFlags (IMAP protocol requirement)
- pass credentials to SyncCoordinator so IDLE monitoring activates
- add selectedItem tracking to MailViewModel, wire List selection in GTD views
- fix startPeriodicSync to sleep-first, preventing duplicate sync on launch
- add deinit cleanup for EventLoopGroup in IMAPConnection, SMTPConnection
- use separate IMAP client for attachment downloads, avoid shared connection interference
- remove [weak self] from IMAPIdleClient actor Task to prevent orphaned connections
- fix isGTDPerspective to check selectedMailbox instead of items.isEmpty
- fix fetchBody to use complete RFC822 fetch instead of BODY[TEXT]
- reuse single IMAP connection per ActionQueue.flush() batch
- add requiresIMAP to ActionPayload for connection batching
- load task categories from label store instead of hardcoded empty array
- suppress NIOSSLHandler Sendable warnings via Package.swift unsafeFlags
- fix unused variable warnings across codebase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:13:46 +01:00
parent 3b82e6cd95
commit 10b7cb2fd2
12 changed files with 140 additions and 82 deletions

View File

@@ -111,7 +111,7 @@ struct ContentView: View {
@ViewBuilder
private var statusBanner: some View {
switch viewModel.syncState {
case .error(let message):
case .error(_):
HStack {
Image(systemName: "wifi.slash")
Text("Offline — showing cached mail")

View File

@@ -59,6 +59,7 @@ final class MailViewModel {
var selectedPerspective: Perspective = .inbox
var items: [ItemSummary] = []
var selectedItem: ItemSummary?
var perspectiveCounts: [Perspective: Int] = [:]
private var imapClientProvider: (() -> IMAPClient)?
@@ -134,7 +135,15 @@ final class MailViewModel {
imapClient: imapClient,
store: mailStore,
actionQueue: queue,
taskStore: ts
taskStore: ts,
credentials: credentials,
imapClientProvider: {
IMAPClient(
host: capturedImapHost,
port: capturedImapPort,
credentials: capturedCredentials
)
}
)
}
@@ -471,7 +480,7 @@ final class MailViewModel {
// MARK: - GTD Triage Actions (unified items)
func deferItem(_ item: ItemSummary, until date: Date?) {
guard let store, let accountConfig else { return }
guard let store, let _ = accountConfig else { return }
do {
switch item {
case .email(let msg):
@@ -498,7 +507,7 @@ final class MailViewModel {
}
func fileItem(_ item: ItemSummary, label: LabelInfo) {
guard let store, let accountConfig else { return }
guard let store, let _ = accountConfig else { return }
do {
switch item {
case .email(let msg):
@@ -674,7 +683,7 @@ final class MailViewModel {
deferUntil: record.deferUntil.flatMap { VTODOParser.parseDate($0) },
createdAt: VTODOParser.parseDate(record.createdAt) ?? Date.distantPast,
linkedMessageId: record.linkedMessageId,
categories: [],
categories: (try? store?.labelsForItem(itemType: "task", itemId: record.id).map(\.name)) ?? [],
isSomeday: record.isSomeday
)
}

View File

@@ -8,15 +8,15 @@ struct ThreadListView: View {
@State var showDeferPicker = false
@State var showLabelPicker = false
/// Whether the current view shows a GTD perspective (vs. a raw mailbox folder)
/// Whether the current view shows a GTD perspective (vs. a raw mailbox folder).
/// Items being empty does not mean we're in folder mode an empty GTD perspective is still GTD.
private var isGTDPerspective: Bool {
// GTD perspectives are active when items are loaded
!viewModel.items.isEmpty || viewModel.selectedMailbox == nil
viewModel.selectedMailbox == nil
}
var body: some View {
Group {
if !viewModel.items.isEmpty {
if isGTDPerspective {
itemListView
} else {
threadListView
@@ -52,7 +52,12 @@ struct ThreadListView: View {
// MARK: - Unified Item List (GTD perspectives)
private var itemListView: some View {
List(viewModel.items) { item in
List(viewModel.items, selection: Binding(
get: { viewModel.selectedItem?.id },
set: { newId in
viewModel.selectedItem = viewModel.items.first { $0.id == newId }
}
)) { item in
ItemRow(item: item)
.tag(item.id)
}
@@ -239,9 +244,11 @@ struct ThreadListView: View {
// MARK: - Helpers
private var selectedItemSummary: ItemSummary? {
// In GTD mode, use the first item (or a future selection mechanism)
// For now, if there's a selected thread, build the email item from it
if let thread = viewModel.selectedThread,
if let selected = viewModel.selectedItem {
return selected
}
// Fallback: in folder view, use selected thread
if let _ = viewModel.selectedThread,
let msg = viewModel.messages.first {
return .email(msg)
}