Przeglądaj źródła

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 2 miesięcy temu
rodzic
commit
f02654feb5

+ 5 - 2
meetings_app/Google/GoogleCalendarClient.swift

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

+ 121 - 8
meetings_app/ViewController.swift

@@ -17,6 +17,7 @@ private enum SidebarPage: Int {
     case video = 2
     case widgets = 3
     case settings = 4
+    case aiCompanion = 5
 }
 
 private enum ZoomJoinMode: Int {
@@ -1645,6 +1646,7 @@ private extension ViewController {
         pageCache[.video] = nil
         pageCache[.widgets] = nil
         pageCache[.settings] = nil
+        pageCache[.aiCompanion] = nil
         showSidebarPage(selectedSidebarPage)
     }
 
@@ -1907,6 +1909,8 @@ private extension ViewController {
             built = makeWidgetsPageContent()
         case .settings:
             built = makeSettingsPageContent()
+        case .aiCompanion:
+            built = makeAiCompanionPageContent()
         }
         pageCache[page] = built
         return built
@@ -1935,6 +1939,105 @@ private extension ViewController {
         return panel
     }
 
+    private func makeAiCompanionPageContent() -> NSView {
+        let panel = NSView()
+        panel.translatesAutoresizingMaskIntoConstraints = false
+        panel.userInterfaceLayoutDirection = .leftToRight
+
+        let contentStack = NSStackView()
+        contentStack.translatesAutoresizingMaskIntoConstraints = false
+        contentStack.userInterfaceLayoutDirection = .leftToRight
+        contentStack.orientation = .vertical
+        contentStack.spacing = 12
+        contentStack.alignment = .width
+        contentStack.distribution = .fill
+
+        let titleLabel = textLabel("Ai companion", font: typography.pageTitle, color: palette.textPrimary)
+        titleLabel.alignment = .left
+        contentStack.addArrangedSubview(titleLabel)
+
+        let subtitle = textLabel("Ended meetings with temporary audio links", font: typography.fieldLabel, color: palette.textSecondary)
+        subtitle.alignment = .left
+        contentStack.addArrangedSubview(subtitle)
+        contentStack.setCustomSpacing(14, after: subtitle)
+
+        let endedMeetings = scheduleCachedMeetings
+            .filter { $0.endDate < Date() }
+            .sorted { $0.endDate > $1.endDate }
+
+        if endedMeetings.isEmpty {
+            let emptyLabel = textLabel(
+                "No ended meetings yet. Audio items will appear here after meetings end.",
+                font: typography.fieldLabel,
+                color: palette.textMuted
+            )
+            emptyLabel.alignment = .left
+            emptyLabel.maximumNumberOfLines = 2
+            emptyLabel.lineBreakMode = .byWordWrapping
+            contentStack.addArrangedSubview(emptyLabel)
+        } else {
+            for meeting in endedMeetings {
+                contentStack.addArrangedSubview(aiCompanionMeetingCard(meeting))
+            }
+        }
+
+        panel.addSubview(contentStack)
+        NSLayoutConstraint.activate([
+            contentStack.leftAnchor.constraint(equalTo: panel.leftAnchor, constant: 28),
+            contentStack.rightAnchor.constraint(equalTo: panel.rightAnchor, constant: -28),
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor),
+            contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
+        ])
+
+        return panel
+    }
+
+    private func aiCompanionMeetingCard(_ meeting: ScheduledMeeting) -> NSView {
+        let card = roundedContainer(cornerRadius: 14, color: palette.sectionCard)
+        card.translatesAutoresizingMaskIntoConstraints = false
+        styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+
+        let stack = NSStackView()
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.orientation = .vertical
+        stack.alignment = .leading
+        stack.spacing = 8
+
+        let title = textLabel(meeting.title, font: NSFont.systemFont(ofSize: 15, weight: .semibold), color: palette.textPrimary)
+        title.alignment = .left
+        title.maximumNumberOfLines = 2
+        title.lineBreakMode = .byTruncatingTail
+
+        let dateText = DateFormatter.localizedString(from: meeting.startDate, dateStyle: .medium, timeStyle: .short)
+        let dateLabel = textLabel("Date: \(dateText)", font: typography.fieldLabel, color: palette.textSecondary)
+        dateLabel.alignment = .left
+
+        let audioLink = mockAudioURLString(for: meeting)
+        let audioLabel = textLabel("Mock Audio: \(audioLink)", font: typography.fieldLabel, color: palette.primaryBlue)
+        audioLabel.alignment = .left
+        audioLabel.maximumNumberOfLines = 2
+        audioLabel.lineBreakMode = .byTruncatingTail
+
+        stack.addArrangedSubview(title)
+        stack.addArrangedSubview(dateLabel)
+        stack.addArrangedSubview(audioLabel)
+
+        card.addSubview(stack)
+        NSLayoutConstraint.activate([
+            stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
+            stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
+            stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
+            stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14)
+        ])
+
+        return card
+    }
+
+    private func mockAudioURLString(for meeting: ScheduledMeeting) -> String {
+        let slug = meeting.id.replacingOccurrences(of: "[^A-Za-z0-9_-]", with: "-", options: .regularExpression)
+        return "https://mock-audio.local/\(slug).mp3"
+    }
+
     private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
         let panel = NSView()
         panel.translatesAutoresizingMaskIntoConstraints = false
@@ -2423,6 +2526,8 @@ private extension ViewController {
             title = "Widgets"
         case .settings:
             title = "Settings"
+        case .aiCompanion:
+            title = "Ai companion"
         }
         view.window?.title = title
     }
@@ -2443,7 +2548,7 @@ private extension ViewController {
     private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
         switch page {
         case .photo: return false
-        case .joinMeetings, .video, .widgets, .settings: return true
+        case .joinMeetings, .video, .widgets, .settings, .aiCompanion: return true
         }
     }
 
@@ -2505,6 +2610,9 @@ private extension ViewController {
         let widgetsRow = sidebarItem("Widgets", icon: "􀏅", page: .widgets, systemSymbolName: "square.grid.2x2.fill")
         menuStack.addArrangedSubview(widgetsRow)
         sidebarRowViews[.widgets] = widgetsRow
+        let aiCompanionRow = sidebarItem("Ai companion", icon: "􀁚", page: .aiCompanion, systemSymbolName: "waveform")
+        menuStack.addArrangedSubview(aiCompanionRow)
+        sidebarRowViews[.aiCompanion] = aiCompanionRow
         menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
         let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
         menuStack.addArrangedSubview(settingsRow)
@@ -6644,15 +6752,15 @@ private extension ViewController {
     }
 
     private func filteredMeetings(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
+        let now = Date()
         switch scheduleFilter {
         case .all:
-            return meetings
+            return meetings.filter { $0.endDate >= now }
         case .today:
-            let start = Calendar.current.startOfDay(for: Date())
+            let start = Calendar.current.startOfDay(for: now)
             let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
         case .week:
-            let now = Date()
             let end = Calendar.current.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
         }
@@ -6660,19 +6768,18 @@ private extension ViewController {
 
     private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
         let calendar = Calendar.current
+        let now = Date()
         switch schedulePageFilter {
         case .all:
-            return meetings
+            return meetings.filter { $0.endDate >= now }
         case .today:
-            let start = calendar.startOfDay(for: Date())
+            let start = calendar.startOfDay(for: now)
             let end = calendar.date(byAdding: .day, value: 1, to: start) ?? start.addingTimeInterval(86400)
             return meetings.filter { $0.startDate >= start && $0.startDate < end }
         case .week:
-            let now = Date()
             let end = calendar.date(byAdding: .day, value: 7, to: now) ?? now.addingTimeInterval(7 * 86400)
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
         case .month:
-            let now = Date()
             let end = calendar.date(byAdding: .month, value: 1, to: now) ?? now.addingTimeInterval(30 * 86400)
             return meetings.filter { $0.startDate >= now && $0.startDate <= end }
         case .customRange:
@@ -6841,6 +6948,7 @@ private extension ViewController {
                         renderScheduleCards(into: stack, meetings: [])
                     }
                     scheduleCachedMeetings = []
+                    pageCache[.aiCompanion] = nil
                     publishWidgetMeetingsSnapshot(from: [])
                     DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
                     MeetingReminderManager.shared.cancelAllReminders()
@@ -6867,6 +6975,7 @@ private extension ViewController {
                     renderScheduleCards(into: stack, meetings: filtered)
                 }
                 scheduleCachedMeetings = meetings
+                pageCache[.aiCompanion] = nil
                 publishWidgetMeetingsSnapshot(from: filtered)
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
                 if storeKitCoordinator.hasPremiumAccess {
@@ -6891,6 +7000,7 @@ private extension ViewController {
                     renderScheduleCards(into: stack, meetings: [])
                 }
                 scheduleCachedMeetings = []
+                pageCache[.aiCompanion] = nil
                 publishWidgetMeetingsSnapshot(from: [])
                 DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
                 MeetingReminderManager.shared.cancelAllReminders()
@@ -6945,6 +7055,7 @@ private extension ViewController {
                     self.pageCache[.joinMeetings] = nil
                     self.pageCache[.photo] = nil
                     self.pageCache[.widgets] = nil
+                    self.pageCache[.aiCompanion] = nil
                     self.showSidebarPage(self.selectedSidebarPage)
                 }
                 await self.loadSchedule()
@@ -6976,6 +7087,7 @@ private extension ViewController {
                     self.pageCache[.video] = nil
                     self.pageCache[.widgets] = nil
                     self.pageCache[.settings] = nil
+                    self.pageCache[.aiCompanion] = nil
                     self.showSidebarPage(self.selectedSidebarPage)
                 }
                 // Ensure desktop widgets refresh immediately with the newly available meetings.
@@ -7033,6 +7145,7 @@ private extension ViewController {
             pageCache[.video] = nil
             pageCache[.widgets] = nil
             pageCache[.settings] = nil
+            pageCache[.aiCompanion] = nil
             showSidebarPage(selectedSidebarPage)
             Task { [weak self] in
                 await self?.loadSchedule()