|
|
@@ -194,9 +194,10 @@ class ViewController: NSViewController {
|
|
194
|
194
|
private weak var scheduleTopicField: NSTextField?
|
|
195
|
195
|
private weak var scheduleDateField: NSTextField?
|
|
196
|
196
|
private weak var scheduleTimeField: NSTextField?
|
|
197
|
|
- private weak var scheduleTimeZoneCombo: NSComboBox?
|
|
|
197
|
+ private weak var scheduleTimeZonePopup: NSPopUpButton?
|
|
198
|
198
|
private weak var scheduleDurationField: NSTextField?
|
|
199
|
199
|
private weak var scheduleSubmitButton: NSButton?
|
|
|
200
|
+ private var scheduleTimeZoneDisplayToIdentifier: [String: String] = [:]
|
|
200
|
201
|
private weak var joinURLField: NSTextField?
|
|
201
|
202
|
private weak var joinMeetingIDField: NSTextField?
|
|
202
|
203
|
private weak var joinPasscodeField: NSTextField?
|
|
|
@@ -670,19 +671,26 @@ class ViewController: NSViewController {
|
|
670
|
671
|
let timeHint = makeLabel("12-hour time in the timezone below · example 2:30 PM", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
671
|
672
|
|
|
672
|
673
|
let tzLabel = makeLabel("Timezone", size: 12, color: secondaryText, weight: .medium, centered: false)
|
|
673
|
|
- let tzCombo = NSComboBox()
|
|
|
674
|
+ let tzCombo = NSPopUpButton()
|
|
674
|
675
|
tzCombo.translatesAutoresizingMaskIntoConstraints = false
|
|
675
|
676
|
tzCombo.font = .systemFont(ofSize: 14, weight: .regular)
|
|
676
|
|
- tzCombo.textColor = primaryText
|
|
677
|
|
- tzCombo.backgroundColor = palette.inputBackground
|
|
678
|
|
- tzCombo.isSelectable = true
|
|
679
|
|
- tzCombo.completes = true
|
|
680
|
|
- tzCombo.numberOfVisibleItems = 10
|
|
681
|
|
- tzCombo.addItems(withObjectValues: TimeZone.knownTimeZoneIdentifiers.sorted())
|
|
682
|
|
- tzCombo.stringValue = TimeZone.current.identifier
|
|
683
|
|
- tzCombo.toolTip = "IANA timezone (type to filter). Meeting start is interpreted in this zone."
|
|
684
|
|
- scheduleTimeZoneCombo = tzCombo
|
|
685
|
|
- let tzHint = makeLabel("Type to search, e.g. America/New_York or Europe/London", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
|
677
|
+ tzCombo.wantsLayer = true
|
|
|
678
|
+ tzCombo.layer?.cornerRadius = 10
|
|
|
679
|
+ tzCombo.layer?.borderWidth = 1
|
|
|
680
|
+ tzCombo.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
681
|
+ tzCombo.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
682
|
+ let tzOptions = makeTimeZoneDisplayOptions(referenceDate: defaultStart)
|
|
|
683
|
+ scheduleTimeZoneDisplayToIdentifier = Dictionary(uniqueKeysWithValues: tzOptions.map { ($0.display, $0.identifier) })
|
|
|
684
|
+ tzCombo.removeAllItems()
|
|
|
685
|
+ tzCombo.addItems(withTitles: tzOptions.map(\.display))
|
|
|
686
|
+ if let selected = tzOptions.first(where: { $0.identifier == TimeZone.current.identifier }) {
|
|
|
687
|
+ tzCombo.selectItem(withTitle: selected.display)
|
|
|
688
|
+ } else {
|
|
|
689
|
+ tzCombo.selectItem(at: 0)
|
|
|
690
|
+ }
|
|
|
691
|
+ tzCombo.toolTip = "Timezone with GMT offset. Meeting start is interpreted in this zone."
|
|
|
692
|
+ scheduleTimeZonePopup = tzCombo
|
|
|
693
|
+ let tzHint = makeLabel("Includes GMT offset, e.g. GMT+05:00 - Asia/Karachi", size: 11, color: mutedText, weight: .regular, centered: false)
|
|
686
|
694
|
|
|
687
|
695
|
let tzStack = NSStackView(views: [tzLabel, tzCombo, tzHint])
|
|
688
|
696
|
tzStack.orientation = .vertical
|
|
|
@@ -789,7 +797,7 @@ class ViewController: NSViewController {
|
|
789
|
797
|
|
|
790
|
798
|
topicBox.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
791
|
799
|
tzCombo.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
792
|
|
- tzCombo.heightAnchor.constraint(equalToConstant: 46),
|
|
|
800
|
+ tzCombo.heightAnchor.constraint(equalToConstant: 34),
|
|
793
|
801
|
dateTimeRow.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
794
|
802
|
durationBox.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
795
|
803
|
buttons.widthAnchor.constraint(equalTo: innerStack.widthAnchor),
|
|
|
@@ -811,11 +819,13 @@ class ViewController: NSViewController {
|
|
811
|
819
|
showSimpleAlert(title: "Topic required", message: "Enter a name for your meeting.")
|
|
812
|
820
|
return
|
|
813
|
821
|
}
|
|
814
|
|
- let tzId = scheduleTimeZoneCombo?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
815
|
|
- if tzId.isEmpty == false, TimeZone(identifier: tzId) == nil {
|
|
|
822
|
+ let tzId = scheduleTimeZonePopup?.titleOfSelectedItem?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
823
|
+ if tzId.isEmpty == false,
|
|
|
824
|
+ scheduleTimeZoneDisplayToIdentifier[tzId] == nil,
|
|
|
825
|
+ TimeZone(identifier: tzId) == nil {
|
|
816
|
826
|
showSimpleAlert(
|
|
817
|
827
|
title: "Timezone",
|
|
818
|
|
- message: "Enter a valid IANA timezone (pick from the list or type to search), for example America/New_York."
|
|
|
828
|
+ message: "Choose a timezone from the list (with GMT offset), for example GMT+05:00 - Asia/Karachi."
|
|
819
|
829
|
)
|
|
820
|
830
|
return
|
|
821
|
831
|
}
|
|
|
@@ -896,18 +906,45 @@ class ViewController: NSViewController {
|
|
896
|
906
|
scheduleTopicField = nil
|
|
897
|
907
|
scheduleDateField = nil
|
|
898
|
908
|
scheduleTimeField = nil
|
|
899
|
|
- scheduleTimeZoneCombo = nil
|
|
|
909
|
+ scheduleTimeZonePopup = nil
|
|
900
|
910
|
scheduleDurationField = nil
|
|
901
|
911
|
scheduleSubmitButton = nil
|
|
|
912
|
+ scheduleTimeZoneDisplayToIdentifier = [:]
|
|
902
|
913
|
}
|
|
903
|
914
|
|
|
904
|
915
|
/// Resolves the schedule panel timezone (IANA id from combo, or system default).
|
|
905
|
916
|
private func resolvedScheduleTimeZoneForMeeting() -> TimeZone {
|
|
906
|
|
- let raw = scheduleTimeZoneCombo?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
917
|
+ let raw = scheduleTimeZonePopup?.titleOfSelectedItem?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
918
|
+ if let identifier = scheduleTimeZoneDisplayToIdentifier[raw],
|
|
|
919
|
+ let mapped = TimeZone(identifier: identifier) {
|
|
|
920
|
+ return mapped
|
|
|
921
|
+ }
|
|
907
|
922
|
guard raw.isEmpty == false, let tz = TimeZone(identifier: raw) else { return TimeZone.current }
|
|
908
|
923
|
return tz
|
|
909
|
924
|
}
|
|
910
|
925
|
|
|
|
926
|
+ private struct TimeZoneDisplayOption {
|
|
|
927
|
+ let identifier: String
|
|
|
928
|
+ let display: String
|
|
|
929
|
+ }
|
|
|
930
|
+
|
|
|
931
|
+ private func makeTimeZoneDisplayOptions(referenceDate: Date) -> [TimeZoneDisplayOption] {
|
|
|
932
|
+ TimeZone.knownTimeZoneIdentifiers.compactMap { identifier in
|
|
|
933
|
+ guard let tz = TimeZone(identifier: identifier) else { return nil }
|
|
|
934
|
+ let seconds = tz.secondsFromGMT(for: referenceDate)
|
|
|
935
|
+ let sign = seconds >= 0 ? "+" : "-"
|
|
|
936
|
+ let absSeconds = abs(seconds)
|
|
|
937
|
+ let hours = absSeconds / 3600
|
|
|
938
|
+ let minutes = (absSeconds % 3600) / 60
|
|
|
939
|
+ let offset = String(format: "GMT%@%02d:%02d", sign, hours, minutes)
|
|
|
940
|
+ return TimeZoneDisplayOption(identifier: identifier, display: "\(offset) - \(identifier)")
|
|
|
941
|
+ }
|
|
|
942
|
+ .sorted { lhs, rhs in
|
|
|
943
|
+ if lhs.display == rhs.display { return lhs.identifier < rhs.identifier }
|
|
|
944
|
+ return lhs.display < rhs.display
|
|
|
945
|
+ }
|
|
|
946
|
+ }
|
|
|
947
|
+
|
|
911
|
948
|
private struct ScheduleMeetingDraft {
|
|
912
|
949
|
let startDate: Date
|
|
913
|
950
|
let startTimeWithOffset: String
|