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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user