diff --git a/Calendr.xcodeproj/project.pbxproj b/Calendr.xcodeproj/project.pbxproj index 7c3fe15b..e52813fc 100644 --- a/Calendr.xcodeproj/project.pbxproj +++ b/Calendr.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 3442769A269B5CA4004CFE1C /* UITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34427699269B5CA4004CFE1C /* UITestCase.swift */; }; 3449402E25C348B20020E664 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449402D25C348B20020E664 /* GeneralSettingsViewController.swift */; }; 3449403225C348C70020E664 /* CalendarPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3449403125C348C70020E664 /* CalendarPickerViewController.swift */; }; + 344A59DB29AEBBBF004F0452 /* EventUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344A59DA29AEBBBF004F0452 /* EventUtils.swift */; }; 3453E6FD28386A84002DCC3C /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3453E6FC28386A84002DCC3C /* Bool.swift */; }; 3453E6FF28393943002DCC3C /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3453E6FE28393943002DCC3C /* ContextMenu.swift */; }; 345DD97326920D1B00294D90 /* CalendarViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 345DD97226920D1B00294D90 /* CalendarViewPreview.swift */; }; @@ -198,6 +199,7 @@ 3442769B269B5CA4004CFE1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3449402D25C348B20020E664 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; 3449403125C348C70020E664 /* CalendarPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarPickerViewController.swift; sourceTree = ""; }; + 344A59DA29AEBBBF004F0452 /* EventUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventUtils.swift; sourceTree = ""; }; 3453E6FC28386A84002DCC3C /* Bool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bool.swift; sourceTree = ""; }; 3453E6FE28393943002DCC3C /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; 345DD97226920D1B00294D90 /* CalendarViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewPreview.swift; sourceTree = ""; }; @@ -429,6 +431,14 @@ path = CalendrUITests; sourceTree = ""; }; + 344A59D929AEBBB0004F0452 /* EventUtils */ = { + isa = PBXGroup; + children = ( + 344A59DA29AEBBBF004F0452 /* EventUtils.swift */, + ); + path = EventUtils; + sourceTree = ""; + }; 347D0F8B25952F89002451EC = { isa = PBXGroup; children = ( @@ -661,9 +671,10 @@ 34ABB9E429A12A720021F3CF /* Events */ = { isa = PBXGroup; children = ( + 34ABB9E129A129F00021F3CF /* ContextMenu */, 34ABB9DE29A129D10021F3CF /* EventDetails */, 34ABB9E329A129FE0021F3CF /* EventList */, - 34ABB9E129A129F00021F3CF /* ContextMenu */, + 344A59D929AEBBB0004F0452 /* EventUtils */, ); path = Events; sourceTree = ""; @@ -919,6 +930,7 @@ 34934CD628E69520009635D4 /* Prefs+UserDefaults.swift in Sources */, 34FD09D925AE269600AAAAE2 /* DateProvider.swift in Sources */, 34D25E4A292F9E2100557E70 /* ImageButton.swift in Sources */, + 344A59DB29AEBBBF004F0452 /* EventUtils.swift in Sources */, 34AC60C626925FA5005312B6 /* PreviewExtensions.swift in Sources */, 3477F3CE25FAE56A008EA888 /* EventDetailsViewModel.swift in Sources */, 3487A43C25E70F5B00FCC7D7 /* NextEventView.swift in Sources */, @@ -1227,7 +1239,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.9.1; PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h"; @@ -1253,7 +1265,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.9.0; + MARKETING_VERSION = 1.9.1; PRODUCT_BUNDLE_IDENTIFIER = br.paker.Calendr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Calendr/Config/Calendr-Bridging-Header.h"; diff --git a/Calendr/Events/EventDetails/EventDetailsViewModel.swift b/Calendr/Events/EventDetails/EventDetailsViewModel.swift index 1506f1e3..5faf2d36 100644 --- a/Calendr/Events/EventDetails/EventDetailsViewModel.swift +++ b/Calendr/Events/EventDetails/EventDetailsViewModel.swift @@ -87,7 +87,13 @@ class EventDetailsViewModel { end = event.end } - duration = formatter.string(from: event.start, to: end) + duration = EventUtils.duration( + from: event.start, + to: end, + timeZone: event.timeZone, + formatter: formatter, + isMeeting: event.isMeeting + ) } } diff --git a/Calendr/Events/EventList/EventViewModel.swift b/Calendr/Events/EventList/EventViewModel.swift index db17e0e0..7de3d25e 100644 --- a/Calendr/Events/EventList/EventViewModel.swift +++ b/Calendr/Events/EventList/EventViewModel.swift @@ -90,7 +90,13 @@ class EventViewModel { end = event.end } - duration = formatter.string(from: event.start, to: end) + duration = EventUtils.duration( + from: event.start, + to: end, + timeZone: event.timeZone, + formatter: formatter, + isMeeting: event.isMeeting + ) } else if !showTime { diff --git a/Calendr/Events/EventUtils/EventUtils.swift b/Calendr/Events/EventUtils/EventUtils.swift new file mode 100644 index 00000000..19a71bad --- /dev/null +++ b/Calendr/Events/EventUtils/EventUtils.swift @@ -0,0 +1,34 @@ +// +// EventUtils.swift +// Calendr +// +// Created by Paker on 28/02/23. +// + +import Foundation + +enum EventUtils { + + static func duration( + from start: Date, + to end: Date, + timeZone: TimeZone?, + formatter: DateIntervalFormatter, + isMeeting: Bool + ) -> String { + + guard + !isMeeting, + let timeZone, timeZone != formatter.timeZone, + let tz_abbreviation = timeZone.abbreviation(for: start) + else { + return formatter.string(from: start, to: end) + } + + let origTimeZone = formatter.timeZone + defer { formatter.timeZone = origTimeZone } + formatter.timeZone = timeZone + + return "\(formatter.string(from: start, to: end)) (\(tz_abbreviation))" + } +} diff --git a/Calendr/Mocks/Factories/EventModel+Factory.swift b/Calendr/Mocks/Factories/EventModel+Factory.swift index b1941f70..16d24814 100644 --- a/Calendr/Mocks/Factories/EventModel+Factory.swift +++ b/Calendr/Mocks/Factories/EventModel+Factory.swift @@ -22,7 +22,8 @@ extension EventModel { isAllDay: Bool = false, type: EventType = .event(.accepted), calendar: CalendarModel = .make(), - participants: [Participant] = [] + participants: [Participant] = [], + timeZone: TimeZone? = nil ) -> EventModel { .init( @@ -36,7 +37,26 @@ extension EventModel { isAllDay: isAllDay || type.isBirthday, type: type, calendar: calendar, - participants: participants + participants: participants, + timeZone: timeZone + ) + } +} + +extension Participant { + + static func make( + name: String = "", + status: EventStatus = .unknown, + isOrganizer: Bool = false, + isCurrentUser: Bool = false + ) -> Participant { + + .init( + name: name, + status: status, + isOrganizer: isOrganizer, + isCurrentUser: isCurrentUser ) } } diff --git a/Calendr/Models/EventModel.swift b/Calendr/Models/EventModel.swift index e9b9deee..8e855223 100644 --- a/Calendr/Models/EventModel.swift +++ b/Calendr/Models/EventModel.swift @@ -19,6 +19,7 @@ struct EventModel: Equatable { let type: EventType let calendar: CalendarModel let participants: [Participant] + let timeZone: TimeZone? } enum EventStatus: Comparable { @@ -60,6 +61,8 @@ extension EventModel { func range(using dateProvider: DateProviding) -> DateRange { .init(start: start, end: end, dateProvider: dateProvider) } var status: EventStatus { if case .event(let status) = type { return status } else { return .unknown } } + + var isMeeting: Bool { !participants.isEmpty } } struct Participant: Hashable { diff --git a/Calendr/Previews/EventDetailsPreview.swift b/Calendr/Previews/EventDetailsPreview.swift index 9fb79140..6b1d3afd 100644 --- a/Calendr/Previews/EventDetailsPreview.swift +++ b/Calendr/Previews/EventDetailsPreview.swift @@ -134,18 +134,18 @@ private extension Strings { private extension Array where Element == Participant { static let mock: Self = [ - .init(name: "Liam", status: .declined, isOrganizer: false, isCurrentUser: false), - .init(name: "Olivia", status: .pending, isOrganizer: false, isCurrentUser: false), - .init(name: "Noah", status: .pending, isOrganizer: false, isCurrentUser: false), - .init(name: "Emma", status: .accepted, isOrganizer: true, isCurrentUser: false), - .init(name: "Carlos", status: .maybe, isOrganizer: false, isCurrentUser: true), - .init(name: "Charlotte", status: .accepted, isOrganizer: false, isCurrentUser: false), - .init(name: "Elijah", status: .accepted, isOrganizer: false, isCurrentUser: false), - .init(name: "Nelson", status: .declined, isOrganizer: false, isCurrentUser: false), - .init(name: "Brian", status: .accepted, isOrganizer: false, isCurrentUser: false), - .init(name: "Oliver", status: .accepted, isOrganizer: false, isCurrentUser: false), - .init(name: "Lucas", status: .accepted, isOrganizer: false, isCurrentUser: false), - .init(name: "Jean", status: .maybe, isOrganizer: false, isCurrentUser: false) + .make(name: "Liam", status: .declined), + .make(name: "Olivia", status: .pending), + .make(name: "Noah", status: .pending), + .make(name: "Emma", status: .accepted, isOrganizer: true), + .make(name: "Carlos", status: .maybe, isCurrentUser: true), + .make(name: "Charlotte", status: .accepted), + .make(name: "Elijah", status: .accepted), + .make(name: "Nelson", status: .declined), + .make(name: "Brian", status: .accepted), + .make(name: "Oliver", status: .accepted), + .make(name: "Lucas", status: .accepted), + .make(name: "Jean", status: .maybe) ] } diff --git a/Calendr/Providers/CalendarServiceProvider.swift b/Calendr/Providers/CalendarServiceProvider.swift index cc8910c8..02855efd 100644 --- a/Calendr/Providers/CalendarServiceProvider.swift +++ b/Calendr/Providers/CalendarServiceProvider.swift @@ -321,7 +321,8 @@ private extension EventModel { isAllDay: event.shouldBeAllDay, type: .init(from: event), calendar: .init(from: event.calendar), - participants: .init(from: event) + participants: .init(from: event), + timeZone: event.timeZone ) } @@ -337,7 +338,8 @@ private extension EventModel { isAllDay: reminder.dueDateComponents!.hour == nil, type: .reminder, calendar: .init(from: reminder.calendar), - participants: [] + participants: [], + timeZone: reminder.timeZone ) } } diff --git a/CalendrTests/EventDetailsViewModelTests.swift b/CalendrTests/EventDetailsViewModelTests.swift index a40951f1..c0e7c015 100644 --- a/CalendrTests/EventDetailsViewModelTests.swift +++ b/CalendrTests/EventDetailsViewModelTests.swift @@ -138,11 +138,11 @@ class EventDetailsViewModelTests: XCTestCase { let viewModel = mock( event: .make( participants: [ - .init(name: "c", status: .unknown, isOrganizer: false, isCurrentUser: false), - .init(name: "b", status: .unknown, isOrganizer: false, isCurrentUser: false), - .init(name: "me", status: .unknown, isOrganizer: false, isCurrentUser: true), - .init(name: "organizer", status: .unknown, isOrganizer: true, isCurrentUser: false), - .init(name: "a", status: .unknown, isOrganizer: false, isCurrentUser: false), + .make(name: "c"), + .make(name: "b"), + .make(name: "me", isCurrentUser: true), + .make(name: "organizer", isOrganizer: true), + .make(name: "a"), ] ) ) diff --git a/CalendrTests/EventViewModelTests.swift b/CalendrTests/EventViewModelTests.swift index 7790b5c0..0fe0c339 100644 --- a/CalendrTests/EventViewModelTests.swift +++ b/CalendrTests/EventViewModelTests.swift @@ -252,6 +252,33 @@ class EventViewModelTests: XCTestCase { XCTAssertEqual(viewModel.duration, "Jan 1 - Feb 2") } + func testDuration_isSameDay_withDifferentTimeZone() { + + let viewModel = mock( + event: .make( + start: .make(year: 2021, month: 1, day: 1, hour: 15), + end: .make(year: 2021, month: 1, day: 1, hour: 16), + timeZone: .init(abbreviation: "GMT-3") + ) + ) + + XCTAssertEqual(viewModel.duration, "12:00 - 1:00 PM (GMT-3)") + } + + func testDuration_isSameDay_isMeeting_withDifferentTimeZone() { + + let viewModel = mock( + event: .make( + start: .make(year: 2021, month: 1, day: 1, hour: 15), + end: .make(year: 2021, month: 1, day: 1, hour: 16), + participants: [.make()], + timeZone: .init(abbreviation: "GMT-3") + ) + ) + + XCTAssertEqual(viewModel.duration, "3:00 - 4:00 PM") + } + func testBarStyle() { XCTAssertEqual(mock(type: .birthday).barStyle, .filled)