Pārlūkot izejas kodu

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 mēnesi atpakaļ
vecāks
revīzija
f02654feb5

+ 5 - 2
meetings_app/Google/GoogleCalendarClient.swift

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

+ 121 - 8
meetings_app/ViewController.swift

@@ -17,6 +17,7 @@ private enum SidebarPage: Int {
17 17
     case video = 2
18 18
     case widgets = 3
19 19
     case settings = 4
20
+    case aiCompanion = 5
20 21
 }
21 22
 
22 23
 private enum ZoomJoinMode: Int {
@@ -1645,6 +1646,7 @@ private extension ViewController {
1645 1646
         pageCache[.video] = nil
1646 1647
         pageCache[.widgets] = nil
1647 1648
         pageCache[.settings] = nil
1649
+        pageCache[.aiCompanion] = nil
1648 1650
         showSidebarPage(selectedSidebarPage)
1649 1651
     }
1650 1652
 
@@ -1907,6 +1909,8 @@ private extension ViewController {
1907 1909
             built = makeWidgetsPageContent()
1908 1910
         case .settings:
1909 1911
             built = makeSettingsPageContent()
1912
+        case .aiCompanion:
1913
+            built = makeAiCompanionPageContent()
1910 1914
         }
1911 1915
         pageCache[page] = built
1912 1916
         return built
@@ -1935,6 +1939,105 @@ private extension ViewController {
1935 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 2041
     private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
1939 2042
         let panel = NSView()
1940 2043
         panel.translatesAutoresizingMaskIntoConstraints = false
@@ -2423,6 +2526,8 @@ private extension ViewController {
2423 2526
             title = "Widgets"
2424 2527
         case .settings:
2425 2528
             title = "Settings"
2529
+        case .aiCompanion:
2530
+            title = "Ai companion"
2426 2531
         }
2427 2532
         view.window?.title = title
2428 2533
     }
@@ -2443,7 +2548,7 @@ private extension ViewController {
2443 2548
     private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
2444 2549
         switch page {
2445 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 2610
         let widgetsRow = sidebarItem("Widgets", icon: "􀏅", page: .widgets, systemSymbolName: "square.grid.2x2.fill")
2506 2611
         menuStack.addArrangedSubview(widgetsRow)
2507 2612
         sidebarRowViews[.widgets] = widgetsRow
2613
+        let aiCompanionRow = sidebarItem("Ai companion", icon: "􀁚", page: .aiCompanion, systemSymbolName: "waveform")
2614
+        menuStack.addArrangedSubview(aiCompanionRow)
2615
+        sidebarRowViews[.aiCompanion] = aiCompanionRow
2508 2616
         menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
2509 2617
         let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
2510 2618
         menuStack.addArrangedSubview(settingsRow)
@@ -6644,15 +6752,15 @@ private extension ViewController {
6644 6752
     }
6645 6753
 
6646 6754
     private func filteredMeetings(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6755
+        let now = Date()
6647 6756
         switch scheduleFilter {
6648 6757
         case .all:
6649
-            return meetings
6758
+            return meetings.filter { $0.endDate >= now }
6650 6759
         case .today:
6651
-            let start = Calendar.current.startOfDay(for: Date())
6760
+            let start = Calendar.current.startOfDay(for: now)
6652 6761
             let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6653 6762
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6654 6763
         case .week:
6655
-            let now = Date()
6656 6764
             let end = Calendar.current.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6657 6765
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6658 6766
         }
@@ -6660,19 +6768,18 @@ private extension ViewController {
6660 6768
 
6661 6769
     private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
6662 6770
         let calendar = Calendar.current
6771
+        let now = Date()
6663 6772
         switch schedulePageFilter {
6664 6773
         case .all:
6665
-            return meetings
6774
+            return meetings.filter { $0.endDate >= now }
6666 6775
         case .today:
6667
-            let start = calendar.startOfDay(for: Date())
6776
+            let start = calendar.startOfDay(for: now)
6668 6777
             let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
6669 6778
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
6670 6779
         case .week:
6671
-            let now = Date()
6672 6780
             let end = calendar.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
6673 6781
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6674 6782
         case .month:
6675
-            let now = Date()
6676 6783
             let end = calendar.date(byAdding: .month, value: 1, to: now) ?? now.addingTimeInterval(30 * 86400)
6677 6784
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
6678 6785
         case .customRange:
@@ -6841,6 +6948,7 @@ private extension ViewController {
6841 6948
                         renderScheduleCards(into: stack, meetings: [])
6842 6949
                     }
6843 6950
                     scheduleCachedMeetings = []
6951
+                    pageCache[.aiCompanion] = nil
6844 6952
                     publishWidgetMeetingsSnapshot(from: [])
6845 6953
                     DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6846 6954
                     MeetingReminderManager.shared.cancelAllReminders()
@@ -6867,6 +6975,7 @@ private extension ViewController {
6867 6975
                     renderScheduleCards(into: stack, meetings: filtered)
6868 6976
                 }
6869 6977
                 scheduleCachedMeetings = meetings
6978
+                pageCache[.aiCompanion] = nil
6870 6979
                 publishWidgetMeetingsSnapshot(from: filtered)
6871 6980
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6872 6981
                 if storeKitCoordinator.hasPremiumAccess {
@@ -6891,6 +7000,7 @@ private extension ViewController {
6891 7000
                     renderScheduleCards(into: stack, meetings: [])
6892 7001
                 }
6893 7002
                 scheduleCachedMeetings = []
7003
+                pageCache[.aiCompanion] = nil
6894 7004
                 publishWidgetMeetingsSnapshot(from: [])
6895 7005
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
6896 7006
                 MeetingReminderManager.shared.cancelAllReminders()
@@ -6945,6 +7055,7 @@ private extension ViewController {
6945 7055
                     self.pageCache[.joinMeetings] = nil
6946 7056
                     self.pageCache[.photo] = nil
6947 7057
                     self.pageCache[.widgets] = nil
7058
+                    self.pageCache[.aiCompanion] = nil
6948 7059
                     self.showSidebarPage(self.selectedSidebarPage)
6949 7060
                 }
6950 7061
                 await self.loadSchedule()
@@ -6976,6 +7087,7 @@ private extension ViewController {
6976 7087
                     self.pageCache[.video] = nil
6977 7088
                     self.pageCache[.widgets] = nil
6978 7089
                     self.pageCache[.settings] = nil
7090
+                    self.pageCache[.aiCompanion] = nil
6979 7091
                     self.showSidebarPage(self.selectedSidebarPage)
6980 7092
                 }
6981 7093
                 // Ensure desktop widgets refresh immediately with the newly available meetings.
@@ -7033,6 +7145,7 @@ private extension ViewController {
7033 7145
             pageCache[.video] = nil
7034 7146
             pageCache[.widgets] = nil
7035 7147
             pageCache[.settings] = nil
7148
+            pageCache[.aiCompanion] = nil
7036 7149
             showSidebarPage(selectedSidebarPage)
7037 7150
             Task { [weak self] in
7038 7151
                 await self?.loadSchedule()