Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Thunderbird/Thunderbird.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
membershipExceptions = (
FeatureFlags/Distribution.swift,
FeatureFlags/FeatureFlags.swift,
Util/SmartDateFormatter.swift,
);
target = 1521D8232D9C4D6300C4DFDF /* ThunderbirdTests */;
};
Expand Down
23 changes: 4 additions & 19 deletions Thunderbird/Thunderbird/EmailDisplay/EmailCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,20 +33,6 @@ struct EmailCellView: View {
self.pinned = email.pinned
}

//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 {
VStack(alignment: .leading) {
HStack {
Expand All @@ -58,11 +45,9 @@ 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))
)
}.padding(.leading, pinned ? 0 : 20)
HStack {
if newEmail {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}

}
}

Expand Down
16 changes: 9 additions & 7 deletions Thunderbird/Thunderbird/FeatureFlags/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Expand All @@ -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) {
Expand Down
36 changes: 36 additions & 0 deletions Thunderbird/Thunderbird/Util/SmartDateFormatter.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}

}
16 changes: 14 additions & 2 deletions Thunderbird/Thunderbird/Util/TempEmailModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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,
Expand All @@ -104,7 +104,19 @@ 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,
pinned: 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()],
Expand Down
28 changes: 14 additions & 14 deletions Thunderbird/ThunderbirdTests/FeatureFlagTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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))
}

}
Loading