ソースを参照

Add status bar Ai Assistant shortcut and Top meetings preview.

Show up to three upcoming meetings below Schedule Meeting with formatted date and title, refresh on menu open, and open join links from the menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 ヶ月 前
コミット
aeca580a0a
2 ファイル変更147 行追加23 行削除
  1. 129 1
      zoom_app/AppDelegate.swift
  2. 18 22
      zoom_app/ViewController.swift

+ 129 - 1
zoom_app/AppDelegate.swift

@@ -23,11 +23,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
         case openMeetings = 11
         case openScheduler = 12
         case openWidgets = 13
-        case openSettings = 14
+        case openAiAssistant = 14
+        case openSettings = 15
         case newMeeting = 20
         case joinMeeting = 21
         case scheduleMeeting = 22
         case stopRecording = 23
+        case openTopMeeting = 40
         case auth = 30
         case quit = 99
     }
@@ -171,6 +173,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
     }
 
     func menuWillOpen(_ menu: NSMenu) {
+        if Thread.isMainThread {
+            MainActor.assumeIsolated {
+                mainViewController()?.menuBarSyncTopMeetingsForStatusMenu()
+            }
+        }
         rebuildStatusMenu()
     }
 
@@ -182,11 +189,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
         appendMenuItem("Open Meetings", tag: .openMeetings, to: menu)
         appendMenuItem("Open Scheduler", tag: .openScheduler, to: menu)
         appendMenuItem("Open Widgets", tag: .openWidgets, to: menu)
+        appendMenuItem("Open Ai Assistant", tag: .openAiAssistant, to: menu)
         appendMenuItem("Open Settings", tag: .openSettings, to: menu)
         menu.addItem(.separator())
         appendMenuItem("New Meeting", tag: .newMeeting, to: menu)
         appendMenuItem("Join Meeting", tag: .joinMeeting, to: menu)
         appendMenuItem("Schedule Meeting", tag: .scheduleMeeting, to: menu)
+        if appendTopMeetingsSection(to: menu) {
+            menu.addItem(.separator())
+        }
         if MeetingRecordingManager.shared.isRecording {
             menu.addItem(.separator())
             appendMenuItem("Stop Recording", tag: .stopRecording, to: menu)
@@ -206,10 +217,41 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
         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
     private func handleStatusMenuAction(_ sender: NSMenuItem) {
         guard let action = MenuItemTag(rawValue: sender.tag) else { return }
 
+        if action == .openTopMeeting {
+            focusMainAppWindow()
+            mainViewController()?.menuBarOpenTopMeeting(joinLink: sender.representedObject as? String)
+            return
+        }
+
         if action == .quit {
             NSApp.terminate(nil)
             return
@@ -227,6 +269,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
             controller.menuBarShowSection(.scheduler)
         case .openWidgets:
             controller.menuBarShowSection(.widgets)
+        case .openAiAssistant:
+            controller.menuBarShowSection(.aiCompanion)
         case .openSettings:
             controller.menuBarShowSection(.settings)
         case .newMeeting:
@@ -249,6 +293,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
             }
         case .auth:
             controller.menuBarTriggerAuthAction()
+        case .openTopMeeting:
+            break
         case .quit:
             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 {
     func userNotificationCenter(
         _ center: UNUserNotificationCenter,

+ 18 - 22
zoom_app/ViewController.swift

@@ -8793,7 +8793,10 @@ class ViewController: NSViewController {
                     id: meeting.meetingID ?? meeting.title,
                     title: meeting.title,
                     timeText: widgetTimeText(for: meeting),
-                    joinLink: meeting.webURL?.absoluteString
+                    joinLink: meeting.webURL?.absoluteString,
+                    startTime: meeting.start.timeIntervalSince1970,
+                    endTime: meeting.end?.timeIntervalSince1970,
+                    timeZoneIdentifier: meeting.meetingTimeZone?.identifier
                 )
             }
         WidgetMeetingStore.save(Array(upcoming))
@@ -9347,6 +9350,20 @@ class ViewController: NSViewController {
         updateSelectedHomeSectionUI()
     }
 
+    func menuBarOpenTopMeeting(joinLink: String?) {
+        if let link = joinLink?.trimmingCharacters(in: .whitespacesAndNewlines),
+           !link.isEmpty,
+           let url = URL(string: link) {
+            openWebURLPreferringInApp(url, zoomMeetingDisplayName: nil, recordingConsentWindowTitle: "Join Meeting")
+            return
+        }
+        menuBarShowSection(.meetings)
+    }
+
+    func menuBarSyncTopMeetingsForStatusMenu() {
+        updateWidgetMeetingStoreFromCurrentMeetings()
+    }
+
     func menuBarTriggerNewMeeting() {
         guard isUserLoggedIn() else {
             showSimpleAlert(title: "Sign in required", message: "Please sign in with Zoom to start a new meeting.")
@@ -11031,27 +11048,6 @@ private struct WidgetInstance: Identifiable, Codable, Hashable {
     }
 }
 
-private struct WidgetMeetingSnapshot: Codable, Identifiable {
-    let id: String
-    let title: String
-    let timeText: String
-    let joinLink: String?
-}
-
-private 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)
-    }
-}
-
 private enum WidgetAppNavigator {
     static func open(target: WidgetQuickAction.Destination) {
         DispatchQueue.main.async {