From 22908cfab76099203440eae8041334aea7f8fb06 Mon Sep 17 00:00:00 2001 From: Ashley Date: Wed, 11 Mar 2026 14:25:11 -0400 Subject: [PATCH] Allow option for full date display --- .../Thunderbird.xcodeproj/project.pbxproj | 1 + .../EmailDisplay/EmailCellView.swift | 28 ++---- .../FeatureFlags/FeatureFlagDebugView.swift | 5 +- .../FeatureFlags/FeatureFlags.swift | 16 ++-- .../Thunderbird/Util/SmartDateFormatter.swift | 36 ++++++++ .../Thunderbird/Util/TempEmailModel.swift | 15 +++- .../ThunderbirdTests/FeatureFlagTests.swift | 28 +++--- .../UtilTests/SmartDateFormatterTests.swift | 85 +++++++++++++++++++ 8 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 Thunderbird/Thunderbird/Util/SmartDateFormatter.swift create mode 100644 Thunderbird/ThunderbirdTests/UtilTests/SmartDateFormatterTests.swift diff --git a/Thunderbird/Thunderbird.xcodeproj/project.pbxproj b/Thunderbird/Thunderbird.xcodeproj/project.pbxproj index fd35aafc..7146359d 100644 --- a/Thunderbird/Thunderbird.xcodeproj/project.pbxproj +++ b/Thunderbird/Thunderbird.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ membershipExceptions = ( FeatureFlags/Distribution.swift, FeatureFlags/FeatureFlags.swift, + Util/SmartDateFormatter.swift, ); target = 1521D8232D9C4D6300C4DFDF /* ThunderbirdTests */; }; diff --git a/Thunderbird/Thunderbird/EmailDisplay/EmailCellView.swift b/Thunderbird/Thunderbird/EmailDisplay/EmailCellView.swift index 9f3f4e2b..8e02a046 100644 --- a/Thunderbird/Thunderbird/EmailDisplay/EmailCellView.swift +++ b/Thunderbird/Thunderbird/EmailDisplay/EmailCellView.swift @@ -8,6 +8,7 @@ import SwiftUI struct EmailCellView: View { + @Environment(FeatureFlags.self) private var flags: FeatureFlags let senderText: String let headerText: String let bodyText: String @@ -30,20 +31,6 @@ struct EmailCellView: View { self.isThread = email.isThread } - //Doesn't display times properly yes - func dateFormatter(date: Date) -> String { - if Calendar.current.isDateInToday(date) { - return date.formatted(date: .omitted, time: .shortened) - } else { - let relativeDateFormatter = DateFormatter() - relativeDateFormatter.timeStyle = .none - relativeDateFormatter.dateStyle = .medium - relativeDateFormatter.doesRelativeDateFormatting = true - return relativeDateFormatter.string(from: date) - } - - } - var body: some View { HStack { if newEmail { @@ -64,11 +51,14 @@ struct EmailCellView: View { .font(.headline) .fontWeight(unread ? .semibold : .regular) Spacer() - Text(dateFormatter(date: dateSent)) - .lineLimit(1) - .font(.footnote) - .truncationMode(.tail) - .foregroundColor(.muted) + Text( + SmartDateFormatter() + .dateFormatter(date: dateSent, isSmartDate: !flags.flagForKey(key: Flag.fullDate.rawValue)) + ) + .lineLimit(1) + .font(.footnote) + .truncationMode(.tail) + .foregroundColor(.muted) } HStack { Text(headerText) diff --git a/Thunderbird/Thunderbird/FeatureFlags/FeatureFlagDebugView.swift b/Thunderbird/Thunderbird/FeatureFlags/FeatureFlagDebugView.swift index 30a49093..e3af480f 100644 --- a/Thunderbird/Thunderbird/FeatureFlags/FeatureFlagDebugView.swift +++ b/Thunderbird/Thunderbird/FeatureFlags/FeatureFlagDebugView.swift @@ -13,7 +13,7 @@ struct FeatureFlagDebugView: View { VStack { Toggle("all_remote_feature_flags", isOn: $allowRemoteFlags).padding() List(flags.featureList, id: \.self) { string in - SettingRowView(string, flags.flagForKey(key: .featureX)) + SettingRowView(string, flags.flagForKey(key: string)) } }.onChange(of: allowRemoteFlags) { flags.setAllowRemoteFlags(allowRemote: allowRemoteFlags) @@ -40,9 +40,8 @@ struct SettingRowView: View { Toggle(flagName, isOn: $isOn) }.onChange(of: isOn) { - flags.setFlagForKey(key: .featureX, val: isOn) + flags.setFlagForKey(key: flagName, val: isOn) } - } } diff --git a/Thunderbird/Thunderbird/FeatureFlags/FeatureFlags.swift b/Thunderbird/Thunderbird/FeatureFlags/FeatureFlags.swift index c8db040a..de05b830 100644 --- a/Thunderbird/Thunderbird/FeatureFlags/FeatureFlags.swift +++ b/Thunderbird/Thunderbird/FeatureFlags/FeatureFlags.swift @@ -11,11 +11,12 @@ private let allowRemoteFlags = "allowRemoteFeatureFlags" public enum Flag: String { case featureX case featureY + case fullDate } @MainActor @Observable final public class FeatureFlags: Sendable { - public var featureList: [String] = ["featureX", "featureY"] + public var featureList: [String] = ["fullDate", "featureX", "featureY"] //False = feature is turned off private var featureSettings: [String: Bool] = [:] public var allowRemote: Bool = true @@ -30,8 +31,8 @@ public enum Flag: String { private func setDefaultFlags(distribution: Distribution) { featureSettings = featureList.reduce( into: [:], - { (dict, number) in - dict[number] = false + { (dict, key) in + dict[key] = false }) let storedSettings = (UserDefaults.standard.dictionary(forKey: defaultsKey) ?? [:]) as! [String: Bool] featureSettings.merge(storedSettings) { (current, new) in new } @@ -46,11 +47,12 @@ public enum Flag: String { } - public func flagForKey(key: Flag) -> Bool { - return featureSettings[key.rawValue] ?? false + public func flagForKey(key: String) -> Bool { + return featureSettings[key] ?? false } - public func setFlagForKey(key: Flag, val: Bool) { - featureSettings[key.rawValue] = val + public func setFlagForKey(key: String, val: Bool) { + featureSettings[key] = val + UserDefaults.standard.setValue(featureSettings, forKey: defaultsKey) } public func setAllowRemoteFlags(allowRemote: Bool) { diff --git a/Thunderbird/Thunderbird/Util/SmartDateFormatter.swift b/Thunderbird/Thunderbird/Util/SmartDateFormatter.swift new file mode 100644 index 00000000..e7366d08 --- /dev/null +++ b/Thunderbird/Thunderbird/Util/SmartDateFormatter.swift @@ -0,0 +1,36 @@ +// +// SmartDateFormatter.swift +// Thunderbird +// +// Created by Ashley Soucar on 3/6/26. +// +import SwiftUI + +@MainActor +public struct SmartDateFormatter { + + func dateFormatter(date: Date, isSmartDate: Bool) -> String { + if isSmartDate { + return smartDate(date: date) + } + return fullDateFormatter(date: date) + } + + private func fullDateFormatter(date: Date) -> String { + return date.formatted(date: .numeric, time: .omitted) + } + + private func smartDate(date: Date) -> String { + if Calendar.autoupdatingCurrent.isDateInToday(date) { + return date.formatted(date: .omitted, time: .shortened) + } else if Calendar.autoupdatingCurrent.isDateInYesterday(date) { + let relativeDateFormatter = RelativeDateTimeFormatter() + return relativeDateFormatter.localizedString(for: date, relativeTo: Date()) + } else if !Calendar.autoupdatingCurrent.isDate(date, equalTo: Date(), toGranularity: .year) { + return date.formatted(.dateTime.month(.abbreviated).day().year()) + } else { + return date.formatted(.dateTime.month(.abbreviated).day()) + } + } + +} diff --git a/Thunderbird/Thunderbird/Util/TempEmailModel.swift b/Thunderbird/Thunderbird/Util/TempEmailModel.swift index 968ae1a7..0e7dfbba 100644 --- a/Thunderbird/Thunderbird/Util/TempEmailModel.swift +++ b/Thunderbird/Thunderbird/Util/TempEmailModel.swift @@ -86,7 +86,7 @@ class TempEmail { recipients: ["Rhea Thunderbird", "Roc Thunderbird Jr", "Roc Thunderbird", "Roc Thunderbird Sr"], headerText: "Email four with a longer set of text", bodyText: "This is some nice long text", - dateSent: Date(timeIntervalSinceNow: -16000), + dateSent: Date(timeIntervalSinceNow: -100000), unread: true, newEmail: false, attachments: nil, @@ -97,7 +97,18 @@ class TempEmail { recipients: ["Rhea Thunderbird", "Roc Thunderbird"], headerText: "Email four with a longer set of text", bodyText: "This is some nice long text", - dateSent: Date(timeIntervalSinceNow: -57000), + dateSent: Date(timeIntervalSinceNow: -257000), + unread: false, + newEmail: false, + attachments: [Data()], + isThread: false + ), + TempEmail( + sender: "Sender6", + recipients: ["Rhea Thunderbird", "Roc Thunderbird"], + headerText: "Email four with a longer set of text", + bodyText: "This is some nice long text", + dateSent: Date(timeIntervalSinceReferenceDate: 51_556_900), unread: false, newEmail: false, attachments: [Data()], diff --git a/Thunderbird/ThunderbirdTests/FeatureFlagTests.swift b/Thunderbird/ThunderbirdTests/FeatureFlagTests.swift index da55aab8..cb17f4e3 100644 --- a/Thunderbird/ThunderbirdTests/FeatureFlagTests.swift +++ b/Thunderbird/ThunderbirdTests/FeatureFlagTests.swift @@ -22,26 +22,26 @@ struct FeatureFlagTests { @MainActor @Test func flagForKeyTest() { FeatureFlags.resetFeatureFlags(distribution: .current) let flags: FeatureFlags = FeatureFlags(distribution: .current) - #expect(flags.flagForKey(key: .featureX) == false) // Expected default - flags.setFlagForKey(key: .featureX, val: true) - #expect(flags.flagForKey(key: .featureX) == true) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == false) // Expected default + flags.setFlagForKey(key: Flag.featureX.rawValue, val: true) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == true) FeatureFlags.resetFeatureFlags() } @MainActor @Test func setFlagForKeyTest() { FeatureFlags.resetFeatureFlags(distribution: .current) let flags: FeatureFlags = FeatureFlags(distribution: .current) - #expect(flags.flagForKey(key: .featureX) == false) // Expected default - #expect(flags.flagForKey(key: .featureY) == false) // Expected default - flags.setFlagForKey(key: .featureX, val: true) - #expect(flags.flagForKey(key: .featureX) == true) - #expect(flags.flagForKey(key: .featureY) == false) - flags.setFlagForKey(key: .featureY, val: true) - #expect(flags.flagForKey(key: .featureX) == true) - #expect(flags.flagForKey(key: .featureY) == true) - flags.setFlagForKey(key: .featureX, val: false) - #expect(flags.flagForKey(key: .featureX) == false) - #expect(flags.flagForKey(key: .featureY) == true) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == false) // Expected default + #expect(flags.flagForKey(key: Flag.featureY.rawValue) == false) // Expected default + flags.setFlagForKey(key: Flag.featureX.rawValue, val: true) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == true) + #expect(flags.flagForKey(key: Flag.featureY.rawValue) == false) + flags.setFlagForKey(key: Flag.featureY.rawValue, val: true) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == true) + #expect(flags.flagForKey(key: Flag.featureY.rawValue) == true) + flags.setFlagForKey(key: Flag.featureX.rawValue, val: false) + #expect(flags.flagForKey(key: Flag.featureX.rawValue) == false) + #expect(flags.flagForKey(key: Flag.featureY.rawValue) == true) FeatureFlags.resetFeatureFlags() } } diff --git a/Thunderbird/ThunderbirdTests/UtilTests/SmartDateFormatterTests.swift b/Thunderbird/ThunderbirdTests/UtilTests/SmartDateFormatterTests.swift new file mode 100644 index 00000000..ea74a827 --- /dev/null +++ b/Thunderbird/ThunderbirdTests/UtilTests/SmartDateFormatterTests.swift @@ -0,0 +1,85 @@ +// +// SmartDateFormatterTests.swift +// ThunderbirdTests +// +// Created by Ashley Soucar on 3/6/26. +// + +import Foundation +import Testing + +@MainActor @Suite("Smart Date Formatter tests") struct SmartDateFormatterTests { + var oldDate: Date + var thisYearDate: Date + var yesterdayDate: Date + var todayDate: Date + + init() async throws { + let calendar = Calendar.autoupdatingCurrent + let year = calendar.component(.year, from: Date()) + var dateCompon = DateComponents() + dateCompon.day = 1 + dateCompon.month = 1 + dateCompon.year = year + dateCompon.timeZone = TimeZone.autoupdatingCurrent + + thisYearDate = calendar.date(from: dateCompon)! //Same Calendar year as now + oldDate = Date(timeIntervalSinceReferenceDate: 51_556_900) // Aug 20, 2002 + yesterdayDate = Date(timeIntervalSinceNow: -86400) // 24 hours previous + todayDate = Date(timeIntervalSinceNow: -300) // 5 minutes ago + } + + @Test func smartDate_OldDateTest() async throws { + #expect( + SmartDateFormatter() + .dateFormatter(date: oldDate, isSmartDate: true) + == oldDate + .formatted(date: .abbreviated, time: .omitted) + ) + } + + @Test func smartDate_ThisYearTest() async throws { + #expect( + SmartDateFormatter() + .dateFormatter(date: thisYearDate, isSmartDate: true) + == thisYearDate + .formatted(.dateTime.day().month(.abbreviated)) + ) + } + + @Test func smartDate_YesterdayTest() async throws { + #expect( + SmartDateFormatter() + .dateFormatter(date: yesterdayDate, isSmartDate: true) == "1 day ago") + } + + @Test func smartDate_TodayTest() async throws { + #expect( + SmartDateFormatter() + .dateFormatter(date: todayDate, isSmartDate: true) + == todayDate + .formatted(date: .omitted, time: .shortened)) + } + + @Test func fullDate_AllDatesTest() async throws { + #expect( + SmartDateFormatter() + .dateFormatter(date: oldDate, isSmartDate: false) == oldDate.formatted(date: .numeric, time: .omitted)) + #expect( + SmartDateFormatter() + .dateFormatter(date: thisYearDate, isSmartDate: false) + == thisYearDate + .formatted(date: .numeric, time: .omitted)) + #expect( + SmartDateFormatter() + .dateFormatter(date: yesterdayDate, isSmartDate: false) + == yesterdayDate + .formatted(date: .numeric, time: .omitted)) + #expect( + SmartDateFormatter() + .dateFormatter(date: todayDate, isSmartDate: false) + == todayDate + .formatted(date: .numeric, time: .omitted)) + } + +}