Просмотр исходного кода

Add Ai companion page with ended meeting audio placeholders.

Introduce a new sidebar page for Ai companion and include recently ended meetings in calendar fetch so post-meeting items appear after calls end.

Made-with: Cursor
huzaifahayat12 1 месяц назад
Родитель
Сommit
f02654feb5
2 измененных файлов с 126 добавлено и 10 удалено
  1. 5 2
      meetings_app/Google/GoogleCalendarClient.swift
  2. 121 8
      meetings_app/ViewController.swift

+ 5 - 2
meetings_app/Google/GoogleCalendarClient.swift

@@ -8,11 +8,13 @@ enum GoogleCalendarClientError: Error {
8
 
8
 
9
 final class GoogleCalendarClient {
9
 final class GoogleCalendarClient {
10
     struct Options: Sendable {
10
     struct Options: Sendable {
11
+        var daysBack: Int
11
         var daysAhead: Int
12
         var daysAhead: Int
12
         var maxResults: Int
13
         var maxResults: Int
13
         var includeNonMeetEvents: Bool
14
         var includeNonMeetEvents: Bool
14
 
15
 
15
-        init(daysAhead: Int = 180, maxResults: Int = 200, includeNonMeetEvents: Bool = true) {
16
+        init(daysBack: Int = 1, daysAhead: Int = 180, maxResults: Int = 200, includeNonMeetEvents: Bool = true) {
17
+            self.daysBack = daysBack
16
             self.daysAhead = daysAhead
18
             self.daysAhead = daysAhead
17
             self.maxResults = maxResults
19
             self.maxResults = maxResults
18
             self.includeNonMeetEvents = includeNonMeetEvents
20
             self.includeNonMeetEvents = includeNonMeetEvents
@@ -31,6 +33,7 @@ final class GoogleCalendarClient {
31
 
33
 
32
     func fetchUpcomingMeetings(accessToken: String, options: Options) async throws -> [ScheduledMeeting] {
34
     func fetchUpcomingMeetings(accessToken: String, options: Options) async throws -> [ScheduledMeeting] {
33
         let now = Date()
35
         let now = Date()
36
+        let start = Calendar.current.date(byAdding: .day, value: -max(0, options.daysBack), to: now) ?? now.addingTimeInterval(-24 * 60 * 60)
34
         let end = Calendar.current.date(byAdding: .day, value: max(1, options.daysAhead), to: now) ?? now.addingTimeInterval(180 * 24 * 60 * 60)
37
         let end = Calendar.current.date(byAdding: .day, value: max(1, options.daysAhead), to: now) ?? now.addingTimeInterval(180 * 24 * 60 * 60)
35
         let formatter = ISO8601DateFormatter()
38
         let formatter = ISO8601DateFormatter()
36
         formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
39
         formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -42,7 +45,7 @@ final class GoogleCalendarClient {
42
         repeat {
45
         repeat {
43
             var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
46
             var components = URLComponents(string: "https://www.googleapis.com/calendar/v3/calendars/primary/events")!
44
             var queryItems = [
47
             var queryItems = [
45
-                URLQueryItem(name: "timeMin", value: formatter.string(from: now)),
48
+                URLQueryItem(name: "timeMin", value: formatter.string(from: start)),
46
                 URLQueryItem(name: "timeMax", value: formatter.string(from: end)),
49
                 URLQueryItem(name: "timeMax", value: formatter.string(from: end)),
47
                 URLQueryItem(name: "singleEvents", value: "true"),
50
                 URLQueryItem(name: "singleEvents", value: "true"),
48
                 URLQueryItem(name: "orderBy", value: "startTime"),
51
                 URLQueryItem(name: "orderBy", value: "startTime"),

+ 121 - 8
meetings_app/ViewController.swift

@@ -17,6 +17,7 @@ private enum SidebarPage: Int {
17
     case video = 2
17
     case video = 2
18
     case widgets = 3
18
     case widgets = 3
19
     case settings = 4
19
     case settings = 4
20
+    case aiCompanion = 5
20
 }
21
 }
21
 
22
 
22
 private enum ZoomJoinMode: Int {
23
 private enum ZoomJoinMode: Int {
@@ -1645,6 +1646,7 @@ private extension ViewController {
1645
         pageCache[.video] = nil
1646
         pageCache[.video] = nil
1646
         pageCache[.widgets] = nil
1647
         pageCache[.widgets] = nil
1647
         pageCache[.settings] = nil
1648
         pageCache[.settings] = nil
1649
+        pageCache[.aiCompanion] = nil
1648
         showSidebarPage(selectedSidebarPage)
1650
         showSidebarPage(selectedSidebarPage)
1649
     }
1651
     }
1650
 
1652
 
@@ -1907,6 +1909,8 @@ private extension ViewController {
1907
             built = makeWidgetsPageContent()
1909
             built = makeWidgetsPageContent()
1908
         case .settings:
1910
         case .settings:
1909
             built = makeSettingsPageContent()
1911
             built = makeSettingsPageContent()
1912
+        case .aiCompanion:
1913
+            built = makeAiCompanionPageContent()
1910
         }
1914
         }
1911
         pageCache[page] = built
1915
         pageCache[page] = built
1912
         return built
1916
         return built
@@ -1935,6 +1939,105 @@ private extension ViewController {
1935
         return panel
1939
         return panel
1936
     }
1940
     }
1937
 
1941
 
1942
+    private func makeAiCompanionPageContent() -> NSView {
1943
+        let panel = NSView()
1944
+        panel.translatesAutoresizingMaskIntoConstraints = false
1945
+        panel.userInterfaceLayoutDirection = .leftToRight
1946
+
1947
+        let contentStack = NSStackView()
1948
+        contentStack.translatesAutoresizingMaskIntoConstraints = false
1949
+        contentStack.userInterfaceLayoutDirection = .leftToRight
1950
+        contentStack.orientation = .vertical
1951
+        contentStack.spacing = 12
1952
+        contentStack.alignment = .width
1953
+        contentStack.distribution = .fill
1954
+
1955
+        let titleLabel = textLabel("Ai companion", font: typography.pageTitle, color: palette.textPrimary)
1956
+        titleLabel.alignment = .left
1957
+        contentStack.addArrangedSubview(titleLabel)
1958
+
1959
+        let subtitle = textLabel("Ended meetings with temporary audio links", font: typography.fieldLabel, color: palette.textSecondary)
1960
+        subtitle.alignment = .left
1961
+        contentStack.addArrangedSubview(subtitle)
1962
+        contentStack.setCustomSpacing(14, after: subtitle)
1963
+
1964
+        let endedMeetings = scheduleCachedMeetings
1965
+            .filter { $0.endDate < Date() }
1966
+            .sorted { $0.endDate > $1.endDate }
1967
+
1968
+        if endedMeetings.isEmpty {
1969
+            let emptyLabel = textLabel(
1970
+                "No ended meetings yet. Audio items will appear here after meetings end.",
1971
+                font: typography.fieldLabel,
1972
+                color: palette.textMuted
1973
+            )
1974
+            emptyLabel.alignment = .left
1975
+            emptyLabel.maximumNumberOfLines = 2
1976
+            emptyLabel.lineBreakMode = .byWordWrapping
1977
+            contentStack.addArrangedSubview(emptyLabel)
1978
+        } else {
1979
+            for meeting in endedMeetings {
1980
+                contentStack.addArrangedSubview(aiCompanionMeetingCard(meeting))
1981
+            }
1982
+        }
1983
+
1984
+        panel.addSubview(contentStack)
1985
+        NSLayoutConstraint.activate([
1986
+            contentStack.leftAnchor.constraint(equalTo: panel.leftAnchor, constant: 28),
1987
+            contentStack.rightAnchor.constraint(equalTo: panel.rightAnchor, constant: -28),
1988
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor),
1989
+            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
1990
+        ])
1991
+
1992
+        return panel
1993
+    }
1994
+
1995
+    private func aiCompanionMeetingCard(_ meeting: ScheduledMeeting) -> NSView {
1996
+        let card = roundedContainer(cornerRadius: 14, color: palette.sectionCard)
1997
+        card.translatesAutoresizingMaskIntoConstraints = false
1998
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
1999
+
2000
+        let stack = NSStackView()
2001
+        stack.translatesAutoresizingMaskIntoConstraints = false
2002
+        stack.orientation = .vertical
2003
+        stack.alignment = .leading
2004
+        stack.spacing = 8
2005
+
2006
+        let title = textLabel(meeting.title, font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: palette.textPrimary)
2007
+        title.alignment = .left
2008
+        title.maximumNumberOfLines = 2
2009
+        title.lineBreakMode = .byTruncatingTail
2010
+
2011
+        let dateText = DateFormatter.localizedString(from: meeting.startDate, dateStyle: .medium, timeStyle: .short)
2012
+        let dateLabel = textLabel("Date: \(dateText)", font: typography.fieldLabel, color: palette.textSecondary)
2013
+        dateLabel.alignment = .left
2014
+
2015
+        let audioLink = mockAudioURLString(for: meeting)
2016
+        let audioLabel = textLabel("Mock Audio: \(audioLink)", font: typography.fieldLabel, color: palette.primaryBlue)
2017
+        audioLabel.alignment = .left
2018
+        audioLabel.maximumNumberOfLines = 2
2019
+        audioLabel.lineBreakMode = .byTruncatingTail
2020
+
2021
+        stack.addArrangedSubview(title)
2022
+        stack.addArrangedSubview(dateLabel)
2023
+        stack.addArrangedSubview(audioLabel)
2024
+
2025
+        card.addSubview(stack)
2026
+        NSLayoutConstraint.activate([
2027
+            stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
2028
+            stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
2029
+            stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
2030
+            stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14)
2031
+        ])
2032
+
2033
+        return card
2034
+    }
2035
+
2036
+    private func mockAudioURLString(for meeting: ScheduledMeeting) -> String {
2037
+        let slug = meeting.id.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression)
2038
+        return "https://mock-audio.local/\(slug).mp3"
2039
+    }
2040
+
1938
     private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
2041
     private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
1939
         let panel = NSView()
2042
         let panel = NSView()
1940
         panel.translatesAutoresizingMaskIntoConstraints = false
2043
         panel.translatesAutoresizingMaskIntoConstraints = false
@@ -2423,6 +2526,8 @@ private extension ViewController {
2423
             title = "Widgets"
2526
             title = "Widgets"
2424
         case .settings:
2527
         case .settings:
2425
             title = "Settings"
2528
             title = "Settings"
2529
+        case .aiCompanion:
2530
+            title = "Ai companion"
2426
         }
2531
         }
2427
         view.window?.title = title
2532
         view.window?.title = title
2428
     }
2533
     }
@@ -2443,7 +2548,7 @@ private extension ViewController {
2443
     private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
2548
     private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
2444
         switch page {
2549
         switch page {
2445
         case .photo: return false
2550
         case .photo: return false
2446
-        case .joinMeetings, .video, .widgets, .settings: return true
2551
+        case .joinMeetings, .video, .widgets, .settings, .aiCompanion: return true
2447
         }
2552
         }
2448
     }
2553
     }
2449
 
2554
 
@@ -2505,6 +2610,9 @@ private extension ViewController {
2505
         let widgetsRow = sidebarItem("Widgets", icon: "􀏅", page: .widgets, systemSymbolName: "square.grid.2x2.fill")
2610
         let widgetsRow = sidebarItem("Widgets", icon: "􀏅", page: .widgets, systemSymbolName: "square.grid.2x2.fill")
2506
         menuStack.addArrangedSubview(widgetsRow)
2611
         menuStack.addArrangedSubview(widgetsRow)
2507
         sidebarRowViews[.widgets] = widgetsRow
2612
         sidebarRowViews[.widgets] = widgetsRow
2613
+        let aiCompanionRow = sidebarItem("Ai companion", icon: "􀁚", page: .aiCompanion, systemSymbolName: "waveform")
2614
+        menuStack.addArrangedSubview(aiCompanionRow)
2615
+        sidebarRowViews[.aiCompanion] = aiCompanionRow
2508
         menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
2616
         menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
2509
         let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
2617
         let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
2510
         menuStack.addArrangedSubview(settingsRow)
2618
         menuStack.addArrangedSubview(settingsRow)
@@ -6644,15 +6752,15 @@ private extension ViewController {
6644
     }
6752
     }
6645
 
6753
 
6646
     private func filteredMeetings(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6754
     private func filteredMeetings(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6755
+        let now = Date()
6647
         switch scheduleFilter {
6756
         switch scheduleFilter {
6648
         case .all:
6757
         case .all:
6649
-            return meetings
6758
+            return meetings.filter { $0.endDate >= now }
6650
         case .today:
6759
         case .today:
6651
-            let start = Calendar.current.startOfDay(for: Date())
6760
+            let start = Calendar.current.startOfDay(for: now)
6652
             let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6761
             let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6653
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6762
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6654
         case .week:
6763
         case .week:
6655
-            let now = Date()
6656
             let end = Calendar.current.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6764
             let end = Calendar.current.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6657
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6765
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6658
         }
6766
         }
@@ -6660,19 +6768,18 @@ private extension ViewController {
6660
 
6768
 
6661
     private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6769
     private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6662
         let calendar = Calendar.current
6770
         let calendar = Calendar.current
6771
+        let now = Date()
6663
         switch schedulePageFilter {
6772
         switch schedulePageFilter {
6664
         case .all:
6773
         case .all:
6665
-            return meetings
6774
+            return meetings.filter { $0.endDate >= now }
6666
         case .today:
6775
         case .today:
6667
-            let start = calendar.startOfDay(for: Date())
6776
+            let start = calendar.startOfDay(for: now)
6668
             let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6777
             let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6669
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6778
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6670
         case .week:
6779
         case .week:
6671
-            let now = Date()
6672
             let end = calendar.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6780
             let end = calendar.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6673
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6781
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6674
         case .month:
6782
         case .month:
6675
-            let now = Date()
6676
             let end = calendar.date(byAdding: .month, value: 1, to: now) ?? now.addingTimeInterval(30 * 86400)
6783
             let end = calendar.date(byAdding: .month, value: 1, to: now) ?? now.addingTimeInterval(30 * 86400)
6677
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6784
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6678
         case .customRange:
6785
         case .customRange:
@@ -6841,6 +6948,7 @@ private extension ViewController {
6841
                         renderScheduleCards(into: stack, meetings: [])
6948
                         renderScheduleCards(into: stack, meetings: [])
6842
                     }
6949
                     }
6843
                     scheduleCachedMeetings = []
6950
                     scheduleCachedMeetings = []
6951
+                    pageCache[.aiCompanion] = nil
6844
                     publishWidgetMeetingsSnapshot(from: [])
6952
                     publishWidgetMeetingsSnapshot(from: [])
6845
                     DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6953
                     DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6846
                     MeetingReminderManager.shared.cancelAllReminders()
6954
                     MeetingReminderManager.shared.cancelAllReminders()
@@ -6867,6 +6975,7 @@ private extension ViewController {
6867
                     renderScheduleCards(into: stack, meetings: filtered)
6975
                     renderScheduleCards(into: stack, meetings: filtered)
6868
                 }
6976
                 }
6869
                 scheduleCachedMeetings = meetings
6977
                 scheduleCachedMeetings = meetings
6978
+                pageCache[.aiCompanion] = nil
6870
                 publishWidgetMeetingsSnapshot(from: filtered)
6979
                 publishWidgetMeetingsSnapshot(from: filtered)
6871
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6980
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6872
                 if storeKitCoordinator.hasPremiumAccess {
6981
                 if storeKitCoordinator.hasPremiumAccess {
@@ -6891,6 +7000,7 @@ private extension ViewController {
6891
                     renderScheduleCards(into: stack, meetings: [])
7000
                     renderScheduleCards(into: stack, meetings: [])
6892
                 }
7001
                 }
6893
                 scheduleCachedMeetings = []
7002
                 scheduleCachedMeetings = []
7003
+                pageCache[.aiCompanion] = nil
6894
                 publishWidgetMeetingsSnapshot(from: [])
7004
                 publishWidgetMeetingsSnapshot(from: [])
6895
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
7005
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6896
                 MeetingReminderManager.shared.cancelAllReminders()
7006
                 MeetingReminderManager.shared.cancelAllReminders()
@@ -6945,6 +7055,7 @@ private extension ViewController {
6945
                     self.pageCache[.joinMeetings] = nil
7055
                     self.pageCache[.joinMeetings] = nil
6946
                     self.pageCache[.photo] = nil
7056
                     self.pageCache[.photo] = nil
6947
                     self.pageCache[.widgets] = nil
7057
                     self.pageCache[.widgets] = nil
7058
+                    self.pageCache[.aiCompanion] = nil
6948
                     self.showSidebarPage(self.selectedSidebarPage)
7059
                     self.showSidebarPage(self.selectedSidebarPage)
6949
                 }
7060
                 }
6950
                 await self.loadSchedule()
7061
                 await self.loadSchedule()
@@ -6976,6 +7087,7 @@ private extension ViewController {
6976
                     self.pageCache[.video] = nil
7087
                     self.pageCache[.video] = nil
6977
                     self.pageCache[.widgets] = nil
7088
                     self.pageCache[.widgets] = nil
6978
                     self.pageCache[.settings] = nil
7089
                     self.pageCache[.settings] = nil
7090
+                    self.pageCache[.aiCompanion] = nil
6979
                     self.showSidebarPage(self.selectedSidebarPage)
7091
                     self.showSidebarPage(self.selectedSidebarPage)
6980
                 }
7092
                 }
6981
                 // Ensure desktop widgets refresh immediately with the newly available meetings.
7093
                 // Ensure desktop widgets refresh immediately with the newly available meetings.
@@ -7033,6 +7145,7 @@ private extension ViewController {
7033
             pageCache[.video] = nil
7145
             pageCache[.video] = nil
7034
             pageCache[.widgets] = nil
7146
             pageCache[.widgets] = nil
7035
             pageCache[.settings] = nil
7147
             pageCache[.settings] = nil
7148
+            pageCache[.aiCompanion] = nil
7036
             showSidebarPage(selectedSidebarPage)
7149
             showSidebarPage(selectedSidebarPage)
7037
             Task { [weak self] in
7150
             Task { [weak self] in
7038
                 await self?.loadSchedule()
7151
                 await self?.loadSchedule()