diff --git a/ElementX/Sources/Other/Extensions/Date.swift b/ElementX/Sources/Other/Extensions/Date.swift index 31842f672..b7d6bc5ba 100644 --- a/ElementX/Sources/Other/Extensions/Date.swift +++ b/ElementX/Sources/Other/Extensions/Date.swift @@ -34,6 +34,27 @@ extension Date { } } + /// Similar to ``formattedMinimal`` but returning "Today" instead of the time and + /// including the year when it the date is from a previous year (rather than over a year ago). + func formattedDateSeparator() -> String { + let calendar = Calendar.current + + if calendar.isDateInToday(self) || calendar.isDateInYesterday(self) { + // Simply "Today" or "Yesterday" if it was today or yesterday. + return DateFormatter.relative.string(from: self) + } else if let sixDaysAgo = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: .now)), + sixDaysAgo <= self { + // The named day if it was in the last 6 days. + return formatted(.dateTime.weekday(.wide)) + } else if calendar.component(.year, from: self) == calendar.component(.year, from: .now) { + // The day and month if it was this year. + return formatted(.dateTime.weekday(.wide).day().month(.wide)) + } else { + // The day, month and year if it is any older. + return formatted(.dateTime.weekday(.wide).day().month(.wide).year()) + } + } + /// The date formatted as just the time, for use in timeline items specifically. func formattedTime() -> String { formatted(date: .omitted, time: .shortened) @@ -44,3 +65,15 @@ extension Date { DateComponents(calendar: .current, year: 2007, month: 1, day: 9, hour: 9, minute: 41).date ?? .now } } + +private extension DateFormatter { + // There doesn't appear to be a way to get "Today" out of + // `Date.RelativeFormatStyle` so use the old way instead 😐 + static let relative: DateFormatter = { + let formatter = DateFormatter() + formatter.doesRelativeDateFormatting = true + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() +} diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift index 9d06a0276..ab5357a1a 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift @@ -12,7 +12,7 @@ struct SeparatorRoomTimelineView: View { let timelineItem: SeparatorRoomTimelineItem var body: some View { - Text(timelineItem.timestamp.formatted(date: .complete, time: .omitted)) + Text(timelineItem.timestamp.formattedDateSeparator()) .font(.compound.bodySMSemibold) .foregroundColor(.compound.textPrimary) .frame(maxWidth: .infinity) diff --git a/UnitTests/Sources/DateTests.swift b/UnitTests/Sources/DateTests.swift index 7e666cee4..b00239689 100644 --- a/UnitTests/Sources/DateTests.swift +++ b/UnitTests/Sources/DateTests.swift @@ -36,6 +36,27 @@ class DateTests: XCTestCase { let theMillennium = calendar.date(from: DateComponents(year: 2000, month: 1, day: 1))! XCTAssertEqual(theMillennium.formattedMinimal(), theMillennium.formatted(.dateTime.year().day().month())) } + + func testDateSeparatorFormatting() { + let today = calendar.date(byAdding: DateComponents(hour: 9, minute: 30), to: startOfToday)! + XCTAssertEqual(today.formattedDateSeparator(), "Today") + + let yesterday = calendar.date(byAdding: .hour, value: 1, to: startOfYesterday)! + XCTAssertEqual(yesterday.formattedDateSeparator(), "Yesterday") + + let nearYesterday = calendar.date(byAdding: DateComponents(hour: -10), to: today)! + XCTAssertEqual(nearYesterday.formattedDateSeparator(), yesterday.formatted(Date.RelativeFormatStyle(presentation: .named, capitalizationContext: .beginningOfSentence))) + + let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: startOfToday)! + XCTAssertEqual(threeDaysAgo.formattedDateSeparator(), threeDaysAgo.formatted(.dateTime.weekday(.wide))) + + // This test will fail during the first 6 days of the year. + let sometimeThisYear = calendar.date(byAdding: .month, value: -10, to: startOfToday)! + XCTAssertEqual(sometimeThisYear.formattedDateSeparator(), sometimeThisYear.formatted(.dateTime.weekday(.wide).day().month(.wide))) + + let theMillennium = calendar.date(from: DateComponents(year: 2000, month: 1, day: 1))! + XCTAssertEqual(theMillennium.formattedDateSeparator(), theMillennium.formatted(.dateTime.weekday(.wide).day().month(.wide).year())) + } } // swiftlint:enable force_unwrapping diff --git a/UnitTests/Sources/DeferredFulfillmentTests.swift b/UnitTests/Sources/DeferredFulfillmentTests.swift index e8edfaf9c..ee9d3233a 100644 --- a/UnitTests/Sources/DeferredFulfillmentTests.swift +++ b/UnitTests/Sources/DeferredFulfillmentTests.swift @@ -9,6 +9,7 @@ @testable import ElementX import XCTest +@MainActor class DeferredFulfillmentTests: XCTestCase { private let observable = SomeObservable() @@ -70,7 +71,8 @@ class DeferredFulfillmentTests: XCTestCase { // MARK: - Helpers -@Observable private class SomeObservable { +@Observable +@MainActor private class SomeObservable { var counter = 0 func setCounter(_ newValue: Int, delay: Duration? = nil) async throws {