|
|
@@ -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()
|