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