diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index 78d1effee..0c2772105 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -607,11 +607,16 @@ struct HomeViewContent: View { .padding(.leading, 15) Spacer() - - Text(DWDateFormatter.sharedInstance.dayOfWeek(from: date)) - .font(.footnote) - .foregroundStyle(Color.dash.tertiaryText) - .padding(.trailing, 15) + + // The unknown-date group (restored shielded history with no + // recoverable date) carries the `.distantPast` sentinel — a + // weekday for it would be fabricated. + if date != .distantPast { + Text(DWDateFormatter.sharedInstance.dayOfWeek(from: date)) + .font(.footnote) + .foregroundStyle(Color.dash.tertiaryText) + .padding(.trailing, 15) + } } .padding(.bottom, 6) } diff --git a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift index ff1a658fe..1f115217b 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeViewModel.swift @@ -548,9 +548,20 @@ class HomeViewModel: ObservableObject { self.txByHash[self.coinJoinWithdrawalSet.id] = item } + // Restored shielded entries with no recoverable date (`hasKnownDate + // == false`, date == .distantPast) collect under one dedicated + // trailing group instead of a spurious epoch-day header; the + // distantPast sentinel makes both sorts place them last. + let unknownDateKey = NSLocalizedString( + "Date unknown", + comment: "History group header for restored shielded operations whose original date is not recoverable") let groupedItems = Dictionary( grouping: items.sorted(by: { $0.date > $1.date }), - by: { DWDateFormatter.sharedInstance.dateOnly(from: $0.date) } + by: { + $0.hasKnownDate + ? DWDateFormatter.sharedInstance.dateOnly(from: $0.date) + : unknownDateKey + } ) let array = groupedItems.compactMap { key, items -> TransactionGroup? in diff --git a/DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift b/DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift index 82b1c1993..69914566d 100644 --- a/DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift +++ b/DashWallet/Sources/UI/Home/Views/ShieldedActivityHistory.swift @@ -76,7 +76,15 @@ struct ShieldedActivityItem: Identifiable { /// Exact fee in duffs, when the entry recorded one. let feeDuffs: UInt64? let blockHeight: UInt64? + /// Sort/grouping key. `Date.distantPast` when `hasKnownDate == false` + /// so unknown-age entries sink to the oldest end of the history — + /// only ever rendered through the `hasKnownDate` gate. let date: Date + /// False for SDK rows with `createdAtMs == 0` — the sentinel the + /// scan-derived (restored) entries carry because chain data holds no + /// per-note block time and the scan clock must not masquerade as + /// one. Render the date as unknown, never as the epoch. + let hasKnownDate: Bool /// Decoded UTF-8 text memo, when the 36-byte Dash memo is kind-1 text. let memoText: String? /// Created identity id (hex) for `identityCreate` entries. @@ -123,7 +131,10 @@ struct ShieldedActivityItem: Identifiable { amountDuffs = (amountCreditsOverride ?? row.amount) / 1000 feeDuffs = row.hasFee ? row.fee / 1000 : nil blockHeight = row.hasBlockHeight ? row.blockHeight : nil - date = Date(timeIntervalSince1970: Double(row.createdAtMs) / 1000.0) + hasKnownDate = row.createdAtMs > 0 + date = hasKnownDate + ? Date(timeIntervalSince1970: Double(row.createdAtMs) / 1000.0) + : .distantPast memoText = Self.decodeTextMemo(row.memo) createdIdentityIdHex = effectiveKind == .identityCreate && row.identityId.count == 32 ? row.identityId.map { String(format: "%02x", $0) }.joined() @@ -275,7 +286,8 @@ struct ShieldedActivityItem: Identifiable { } var shortTimeString: String { - DWDateFormatter.sharedInstance.timeOnly(from: date) + guard hasKnownDate else { return "" } + return DWDateFormatter.sharedInstance.timeOnly(from: date) } /// Decode the 36-byte Dash memo when it is kind-1 UTF-8 text: @@ -378,7 +390,9 @@ struct ShieldedActivityDetailsView: View { } infoRow( NSLocalizedString("Date", comment: ""), - DWDateFormatter.sharedInstance.longString(from: item.date)) + item.hasKnownDate + ? DWDateFormatter.sharedInstance.longString(from: item.date) + : NSLocalizedString("Unknown", comment: "Restored shielded operation whose original date is not recoverable")) } .padding(.horizontal, 16) .padding(.vertical, 6) diff --git a/DashWallet/Sources/UI/Home/Views/TransactionListDataItem.swift b/DashWallet/Sources/UI/Home/Views/TransactionListDataItem.swift index 3999ab83d..1c8c23180 100644 --- a/DashWallet/Sources/UI/Home/Views/TransactionListDataItem.swift +++ b/DashWallet/Sources/UI/Home/Views/TransactionListDataItem.swift @@ -70,4 +70,17 @@ extension TransactionListDataItem: Identifiable { return item.date } } + + /// False only for restored shielded entries whose original date is + /// not recoverable (SDK `createdAtMs == 0`); their `date` is the + /// `.distantPast` sort sentinel and must not be rendered or used as + /// a day-group key. + var hasKnownDate: Bool { + switch self { + case .shieldedActivity(let item): + return item.hasKnownDate + default: + return true + } + } } diff --git a/DashWallet/en.lproj/Localizable.strings b/DashWallet/en.lproj/Localizable.strings index 9a05170dc..6876105c0 100644 --- a/DashWallet/en.lproj/Localizable.strings +++ b/DashWallet/en.lproj/Localizable.strings @@ -991,6 +991,9 @@ /* No comment provided by engineer. */ "Date" = "Date"; +/* History group header for restored shielded operations whose original date is not recoverable */ +"Date unknown" = "Date unknown"; + /* Voting */ "Date: New to old" = "Date: New to old";