|
@@ -23,11 +23,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
case openMeetings = 11
|
|
case openMeetings = 11
|
|
|
case openScheduler = 12
|
|
case openScheduler = 12
|
|
|
case openWidgets = 13
|
|
case openWidgets = 13
|
|
|
- case openSettings = 14
|
|
|
|
|
|
|
+ case openAiAssistant = 14
|
|
|
|
|
+ case openSettings = 15
|
|
|
case newMeeting = 20
|
|
case newMeeting = 20
|
|
|
case joinMeeting = 21
|
|
case joinMeeting = 21
|
|
|
case scheduleMeeting = 22
|
|
case scheduleMeeting = 22
|
|
|
case stopRecording = 23
|
|
case stopRecording = 23
|
|
|
|
|
+ case openTopMeeting = 40
|
|
|
case auth = 30
|
|
case auth = 30
|
|
|
case quit = 99
|
|
case quit = 99
|
|
|
}
|
|
}
|
|
@@ -171,6 +173,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func menuWillOpen(_ menu: NSMenu) {
|
|
func menuWillOpen(_ menu: NSMenu) {
|
|
|
|
|
+ if Thread.isMainThread {
|
|
|
|
|
+ MainActor.assumeIsolated {
|
|
|
|
|
+ mainViewController()?.menuBarSyncTopMeetingsForStatusMenu()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
rebuildStatusMenu()
|
|
rebuildStatusMenu()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -182,11 +189,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
appendMenuItem("Open Meetings", tag: .openMeetings, to: menu)
|
|
appendMenuItem("Open Meetings", tag: .openMeetings, to: menu)
|
|
|
appendMenuItem("Open Scheduler", tag: .openScheduler, to: menu)
|
|
appendMenuItem("Open Scheduler", tag: .openScheduler, to: menu)
|
|
|
appendMenuItem("Open Widgets", tag: .openWidgets, to: menu)
|
|
appendMenuItem("Open Widgets", tag: .openWidgets, to: menu)
|
|
|
|
|
+ appendMenuItem("Open Ai Assistant", tag: .openAiAssistant, to: menu)
|
|
|
appendMenuItem("Open Settings", tag: .openSettings, to: menu)
|
|
appendMenuItem("Open Settings", tag: .openSettings, to: menu)
|
|
|
menu.addItem(.separator())
|
|
menu.addItem(.separator())
|
|
|
appendMenuItem("New Meeting", tag: .newMeeting, to: menu)
|
|
appendMenuItem("New Meeting", tag: .newMeeting, to: menu)
|
|
|
appendMenuItem("Join Meeting", tag: .joinMeeting, to: menu)
|
|
appendMenuItem("Join Meeting", tag: .joinMeeting, to: menu)
|
|
|
appendMenuItem("Schedule Meeting", tag: .scheduleMeeting, to: menu)
|
|
appendMenuItem("Schedule Meeting", tag: .scheduleMeeting, to: menu)
|
|
|
|
|
+ if appendTopMeetingsSection(to: menu) {
|
|
|
|
|
+ menu.addItem(.separator())
|
|
|
|
|
+ }
|
|
|
if MeetingRecordingManager.shared.isRecording {
|
|
if MeetingRecordingManager.shared.isRecording {
|
|
|
menu.addItem(.separator())
|
|
menu.addItem(.separator())
|
|
|
appendMenuItem("Stop Recording", tag: .stopRecording, to: menu)
|
|
appendMenuItem("Stop Recording", tag: .stopRecording, to: menu)
|
|
@@ -206,10 +217,41 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
menu.addItem(item)
|
|
menu.addItem(item)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @discardableResult
|
|
|
|
|
+ private func appendTopMeetingsSection(to menu: NSMenu) -> Bool {
|
|
|
|
|
+ let meetings = Array(WidgetMeetingStore.load().prefix(3))
|
|
|
|
|
+ guard meetings.isEmpty == false else { return false }
|
|
|
|
|
+
|
|
|
|
|
+ menu.addItem(.separator())
|
|
|
|
|
+
|
|
|
|
|
+ let header = NSMenuItem(title: "Top meetings", action: nil, keyEquivalent: "")
|
|
|
|
|
+ header.isEnabled = false
|
|
|
|
|
+ menu.addItem(header)
|
|
|
|
|
+
|
|
|
|
|
+ for meeting in meetings {
|
|
|
|
|
+ let item = NSMenuItem(
|
|
|
|
|
+ title: TopMeetingsStatusBarFormatter.menuLine(for: meeting),
|
|
|
|
|
+ action: #selector(handleStatusMenuAction(_:)),
|
|
|
|
|
+ keyEquivalent: ""
|
|
|
|
|
+ )
|
|
|
|
|
+ item.target = self
|
|
|
|
|
+ item.tag = MenuItemTag.openTopMeeting.rawValue
|
|
|
|
|
+ item.representedObject = meeting.joinLink
|
|
|
|
|
+ menu.addItem(item)
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@objc
|
|
@objc
|
|
|
private func handleStatusMenuAction(_ sender: NSMenuItem) {
|
|
private func handleStatusMenuAction(_ sender: NSMenuItem) {
|
|
|
guard let action = MenuItemTag(rawValue: sender.tag) else { return }
|
|
guard let action = MenuItemTag(rawValue: sender.tag) else { return }
|
|
|
|
|
|
|
|
|
|
+ if action == .openTopMeeting {
|
|
|
|
|
+ focusMainAppWindow()
|
|
|
|
|
+ mainViewController()?.menuBarOpenTopMeeting(joinLink: sender.representedObject as? String)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if action == .quit {
|
|
if action == .quit {
|
|
|
NSApp.terminate(nil)
|
|
NSApp.terminate(nil)
|
|
|
return
|
|
return
|
|
@@ -227,6 +269,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
controller.menuBarShowSection(.scheduler)
|
|
controller.menuBarShowSection(.scheduler)
|
|
|
case .openWidgets:
|
|
case .openWidgets:
|
|
|
controller.menuBarShowSection(.widgets)
|
|
controller.menuBarShowSection(.widgets)
|
|
|
|
|
+ case .openAiAssistant:
|
|
|
|
|
+ controller.menuBarShowSection(.aiCompanion)
|
|
|
case .openSettings:
|
|
case .openSettings:
|
|
|
controller.menuBarShowSection(.settings)
|
|
controller.menuBarShowSection(.settings)
|
|
|
case .newMeeting:
|
|
case .newMeeting:
|
|
@@ -249,6 +293,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
}
|
|
}
|
|
|
case .auth:
|
|
case .auth:
|
|
|
controller.menuBarTriggerAuthAction()
|
|
controller.menuBarTriggerAuthAction()
|
|
|
|
|
+ case .openTopMeeting:
|
|
|
|
|
+ break
|
|
|
case .quit:
|
|
case .quit:
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
@@ -289,6 +335,88 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Shared with ViewController and widgets (public = visible across files with Member Import Visibility).
|
|
|
|
|
+public struct WidgetMeetingSnapshot: Codable, Identifiable {
|
|
|
|
|
+ public let id: String
|
|
|
|
|
+ public let title: String
|
|
|
|
|
+ public let timeText: String
|
|
|
|
|
+ public let joinLink: String?
|
|
|
|
|
+ public var startTime: TimeInterval?
|
|
|
|
|
+ public var endTime: TimeInterval?
|
|
|
|
|
+ public var timeZoneIdentifier: String?
|
|
|
|
|
+
|
|
|
|
|
+ public init(
|
|
|
|
|
+ id: String,
|
|
|
|
|
+ title: String,
|
|
|
|
|
+ timeText: String,
|
|
|
|
|
+ joinLink: String?,
|
|
|
|
|
+ startTime: TimeInterval? = nil,
|
|
|
|
|
+ endTime: TimeInterval? = nil,
|
|
|
|
|
+ timeZoneIdentifier: String? = nil
|
|
|
|
|
+ ) {
|
|
|
|
|
+ self.id = id
|
|
|
|
|
+ self.title = title
|
|
|
|
|
+ self.timeText = timeText
|
|
|
|
|
+ self.joinLink = joinLink
|
|
|
|
|
+ self.startTime = startTime
|
|
|
|
|
+ self.endTime = endTime
|
|
|
|
|
+ self.timeZoneIdentifier = timeZoneIdentifier
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public enum WidgetMeetingStore {
|
|
|
|
|
+ static let key = "zoom_app.widget.topMeetings"
|
|
|
|
|
+
|
|
|
|
|
+ static func load() -> [WidgetMeetingSnapshot] {
|
|
|
|
|
+ guard let data = UserDefaults.standard.data(forKey: key) else { return [] }
|
|
|
|
|
+ return (try? JSONDecoder().decode([WidgetMeetingSnapshot].self, from: data)) ?? []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static func save(_ meetings: [WidgetMeetingSnapshot]) {
|
|
|
|
|
+ guard let data = try? JSONEncoder().encode(meetings) else { return }
|
|
|
|
|
+ UserDefaults.standard.set(data, forKey: key)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+public enum TopMeetingsStatusBarFormatter {
|
|
|
|
|
+ public static func menuLine(for meeting: WidgetMeetingSnapshot) -> String {
|
|
|
|
|
+ let titlePart = truncatedTitle(meeting.title, maxLength: 10)
|
|
|
|
|
+
|
|
|
|
|
+ guard let startInterval = meeting.startTime else {
|
|
|
|
|
+ return "\(meeting.timeText) \(titlePart)"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let timeZone = meeting.timeZoneIdentifier.flatMap(TimeZone.init(identifier:)) ?? .current
|
|
|
|
|
+ let start = Date(timeIntervalSince1970: startInterval)
|
|
|
|
|
+ let end = meeting.endTime.map { Date(timeIntervalSince1970: $0) } ?? start.addingTimeInterval(3600)
|
|
|
|
|
+
|
|
|
|
|
+ let dayFormatter = DateFormatter()
|
|
|
|
|
+ dayFormatter.dateFormat = "EEE"
|
|
|
|
|
+ dayFormatter.timeZone = timeZone
|
|
|
|
|
+
|
|
|
|
|
+ let timeFormatter = DateFormatter()
|
|
|
|
|
+ timeFormatter.dateFormat = "h:mm a"
|
|
|
|
|
+ timeFormatter.timeZone = timeZone
|
|
|
|
|
+
|
|
|
|
|
+ let dateFormatter = DateFormatter()
|
|
|
|
|
+ dateFormatter.dateFormat = "d MMM"
|
|
|
|
|
+ dateFormatter.timeZone = timeZone
|
|
|
|
|
+
|
|
|
|
|
+ let day = dayFormatter.string(from: start)
|
|
|
|
|
+ let startText = timeFormatter.string(from: start)
|
|
|
|
|
+ let endText = timeFormatter.string(from: end)
|
|
|
|
|
+ let dateText = dateFormatter.string(from: start)
|
|
|
|
|
+
|
|
|
|
|
+ return "\(day), \(startText) - \(endText) \(dateText) \(titlePart)"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static func truncatedTitle(_ title: String, maxLength: Int) -> String {
|
|
|
|
|
+ let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ guard trimmed.count > maxLength else { return trimmed }
|
|
|
|
|
+ return String(trimmed.prefix(maxLength)) + "..."
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
|
|
func userNotificationCenter(
|
|
func userNotificationCenter(
|
|
|
_ center: UNUserNotificationCenter,
|
|
_ center: UNUserNotificationCenter,
|