|
@@ -15,7 +15,8 @@ private enum SidebarPage: Int {
|
|
|
case joinMeetings = 0
|
|
case joinMeetings = 0
|
|
|
case photo = 1
|
|
case photo = 1
|
|
|
case video = 2
|
|
case video = 2
|
|
|
- case settings = 3
|
|
|
|
|
|
|
+ case widgets = 3
|
|
|
|
|
+ case settings = 4
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private enum ZoomJoinMode: Int {
|
|
private enum ZoomJoinMode: Int {
|
|
@@ -353,6 +354,7 @@ final class ViewController: NSViewController {
|
|
|
private var scheduleProfileImageTask: Task<Void, Never>?
|
|
private var scheduleProfileImageTask: Task<Void, Never>?
|
|
|
private var googleAccountPopover: NSPopover?
|
|
private var googleAccountPopover: NSPopover?
|
|
|
private var scheduleCachedMeetings: [ScheduledMeeting] = []
|
|
private var scheduleCachedMeetings: [ScheduledMeeting] = []
|
|
|
|
|
+ private let widgetSnapshotLimit: Int = 3
|
|
|
|
|
|
|
|
private var schedulePageFilter: SchedulePageFilter = .all
|
|
private var schedulePageFilter: SchedulePageFilter = .all
|
|
|
private var schedulePageFromDate: Date = Calendar.current.startOfDay(for: Date())
|
|
private var schedulePageFromDate: Date = Calendar.current.startOfDay(for: Date())
|
|
@@ -446,6 +448,7 @@ final class ViewController: NSViewController {
|
|
|
migrateLegacyRatingStateIfNeeded()
|
|
migrateLegacyRatingStateIfNeeded()
|
|
|
beginUsageTrackingSessionIfNeeded()
|
|
beginUsageTrackingSessionIfNeeded()
|
|
|
observeAppLifecycleForUsageTrackingIfNeeded()
|
|
observeAppLifecycleForUsageTrackingIfNeeded()
|
|
|
|
|
+ registerWidgetNotificationObservers()
|
|
|
setupRootView()
|
|
setupRootView()
|
|
|
buildMainLayout()
|
|
buildMainLayout()
|
|
|
showLaunchSplashIfNeeded()
|
|
showLaunchSplashIfNeeded()
|
|
@@ -454,6 +457,7 @@ final class ViewController: NSViewController {
|
|
|
|
|
|
|
|
override func viewDidAppear() {
|
|
override func viewDidAppear() {
|
|
|
super.viewDidAppear()
|
|
super.viewDidAppear()
|
|
|
|
|
+ DesktopWidgetWindowManager.shared.restore()
|
|
|
hasViewAppearedOnce = true
|
|
hasViewAppearedOnce = true
|
|
|
presentLaunchPaywallIfNeeded()
|
|
presentLaunchPaywallIfNeeded()
|
|
|
dismissLaunchSplashIfReady()
|
|
dismissLaunchSplashIfReady()
|
|
@@ -494,9 +498,7 @@ final class ViewController: NSViewController {
|
|
|
endUsageTrackingSession()
|
|
endUsageTrackingSession()
|
|
|
launchSplashTimeoutWorkItem?.cancel()
|
|
launchSplashTimeoutWorkItem?.cancel()
|
|
|
launchSplashMinimumDelayWorkItem?.cancel()
|
|
launchSplashMinimumDelayWorkItem?.cancel()
|
|
|
- if hasObservedAppLifecycleForUsage {
|
|
|
|
|
- NotificationCenter.default.removeObserver(self)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ NotificationCenter.default.removeObserver(self)
|
|
|
if let observer = schedulePageScrollObservation {
|
|
if let observer = schedulePageScrollObservation {
|
|
|
NotificationCenter.default.removeObserver(observer)
|
|
NotificationCenter.default.removeObserver(observer)
|
|
|
}
|
|
}
|
|
@@ -685,6 +687,35 @@ private extension ViewController {
|
|
|
showSidebarPage(page)
|
|
showSidebarPage(page)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func registerWidgetNotificationObservers() {
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenJoinMeetingsPageRequested), name: .widgetOpenJoinMeetingsPage, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenSchedulePageRequested), name: .widgetOpenSchedulePage, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenCalendarPageRequested), name: .widgetOpenCalendarPage, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenSettingsPageRequested), name: .widgetOpenSettingsPage, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetSignInRequested), name: .widgetSignInRequested, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetRefreshRequested), name: .widgetRefreshRequested, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenMeetWebRequested), name: .widgetOpenMeetWebRequested, object: nil)
|
|
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(widgetOpenMeetingLinkRequested(_:)), name: .widgetOpenMeetingLinkRequested, object: nil)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func widgetOpenJoinMeetingsPageRequested() { showSidebarPage(.joinMeetings) }
|
|
|
|
|
+ @objc private func widgetOpenSchedulePageRequested() { showSidebarPage(.photo) }
|
|
|
|
|
+ @objc private func widgetOpenCalendarPageRequested() { showSidebarPage(.video) }
|
|
|
|
|
+ @objc private func widgetOpenSettingsPageRequested() { showSidebarPage(.settings) }
|
|
|
|
|
+ @objc private func widgetSignInRequested() { scheduleConnectClicked() }
|
|
|
|
|
+ @objc private func widgetRefreshRequested() { scheduleReloadClicked() }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func widgetOpenMeetWebRequested() {
|
|
|
|
|
+ guard let url = URL(string: "https://meet.google.com/") else { return }
|
|
|
|
|
+ openInDefaultBrowser(url: url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func widgetOpenMeetingLinkRequested(_ notification: Notification) {
|
|
|
|
|
+ guard let link = notification.userInfo?["link"] as? String,
|
|
|
|
|
+ let url = URL(string: link.trimmingCharacters(in: .whitespacesAndNewlines)) else { return }
|
|
|
|
|
+ openInDefaultBrowser(url: url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@objc private func joinMeetClicked(_ sender: Any?) {
|
|
@objc private func joinMeetClicked(_ sender: Any?) {
|
|
|
let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
|
|
|
|
|
@@ -1558,6 +1589,7 @@ private extension ViewController {
|
|
|
pageCache[.joinMeetings] = nil
|
|
pageCache[.joinMeetings] = nil
|
|
|
pageCache[.photo] = nil
|
|
pageCache[.photo] = nil
|
|
|
pageCache[.video] = nil
|
|
pageCache[.video] = nil
|
|
|
|
|
+ pageCache[.widgets] = nil
|
|
|
pageCache[.settings] = nil
|
|
pageCache[.settings] = nil
|
|
|
showSidebarPage(selectedSidebarPage)
|
|
showSidebarPage(selectedSidebarPage)
|
|
|
}
|
|
}
|
|
@@ -1817,6 +1849,8 @@ private extension ViewController {
|
|
|
built = makeSchedulePageContent()
|
|
built = makeSchedulePageContent()
|
|
|
case .video:
|
|
case .video:
|
|
|
built = makeCalendarPageContent()
|
|
built = makeCalendarPageContent()
|
|
|
|
|
+ case .widgets:
|
|
|
|
|
+ built = makeWidgetsPageContent()
|
|
|
case .settings:
|
|
case .settings:
|
|
|
built = makeSettingsPageContent()
|
|
built = makeSettingsPageContent()
|
|
|
}
|
|
}
|
|
@@ -1824,6 +1858,12 @@ private extension ViewController {
|
|
|
return built
|
|
return built
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func makeWidgetsPageContent() -> NSView {
|
|
|
|
|
+ makeWidgetsPageHost(canAddWidgets: storeKitCoordinator.hasPremiumAccess) { [weak self] in
|
|
|
|
|
+ self?.showPaywall()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
|
|
private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
|
|
|
let panel = NSView()
|
|
let panel = NSView()
|
|
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -2308,6 +2348,8 @@ private extension ViewController {
|
|
|
title = "Schedule"
|
|
title = "Schedule"
|
|
|
case .video:
|
|
case .video:
|
|
|
title = "Calendar"
|
|
title = "Calendar"
|
|
|
|
|
+ case .widgets:
|
|
|
|
|
+ title = "Widgets"
|
|
|
case .settings:
|
|
case .settings:
|
|
|
title = "Settings"
|
|
title = "Settings"
|
|
|
}
|
|
}
|
|
@@ -2330,7 +2372,7 @@ private extension ViewController {
|
|
|
private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
|
|
private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
|
|
|
switch page {
|
|
switch page {
|
|
|
case .photo: return false
|
|
case .photo: return false
|
|
|
- case .joinMeetings, .video, .settings: return true
|
|
|
|
|
|
|
+ case .joinMeetings, .video, .widgets, .settings: return true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2389,6 +2431,9 @@ private extension ViewController {
|
|
|
let videoRow = sidebarItem("Calendar", icon: "", page: .video, systemSymbolName: "calendar")
|
|
let videoRow = sidebarItem("Calendar", icon: "", page: .video, systemSymbolName: "calendar")
|
|
|
menuStack.addArrangedSubview(videoRow)
|
|
menuStack.addArrangedSubview(videoRow)
|
|
|
sidebarRowViews[.video] = videoRow
|
|
sidebarRowViews[.video] = videoRow
|
|
|
|
|
+ let widgetsRow = sidebarItem("Widgets", icon: "", page: .widgets, systemSymbolName: "square.grid.2x2.fill")
|
|
|
|
|
+ menuStack.addArrangedSubview(widgetsRow)
|
|
|
|
|
+ sidebarRowViews[.widgets] = widgetsRow
|
|
|
menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
|
|
menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
|
|
|
let settingsRow = sidebarItem("Settings", icon: "", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
|
|
let settingsRow = sidebarItem("Settings", icon: "", page: .settings, systemSymbolName: "gearshape.fill", logoHeightMultiplier: 1, showsDisclosure: true)
|
|
|
menuStack.addArrangedSubview(settingsRow)
|
|
menuStack.addArrangedSubview(settingsRow)
|
|
@@ -6725,6 +6770,8 @@ private extension ViewController {
|
|
|
renderScheduleCards(into: stack, meetings: [])
|
|
renderScheduleCards(into: stack, meetings: [])
|
|
|
}
|
|
}
|
|
|
scheduleCachedMeetings = []
|
|
scheduleCachedMeetings = []
|
|
|
|
|
+ publishWidgetMeetingsSnapshot(from: [])
|
|
|
|
|
+ DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
|
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
|
applySchedulePageFiltersAndRender()
|
|
applySchedulePageFiltersAndRender()
|
|
|
if calendarPageGridStack != nil {
|
|
if calendarPageGridStack != nil {
|
|
@@ -6749,6 +6796,8 @@ private extension ViewController {
|
|
|
renderScheduleCards(into: stack, meetings: filtered)
|
|
renderScheduleCards(into: stack, meetings: filtered)
|
|
|
}
|
|
}
|
|
|
scheduleCachedMeetings = meetings
|
|
scheduleCachedMeetings = meetings
|
|
|
|
|
+ publishWidgetMeetingsSnapshot(from: filtered)
|
|
|
|
|
+ DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
|
|
|
if storeKitCoordinator.hasPremiumAccess {
|
|
if storeKitCoordinator.hasPremiumAccess {
|
|
|
MeetingReminderManager.shared.scheduleReminders(for: meetings)
|
|
MeetingReminderManager.shared.scheduleReminders(for: meetings)
|
|
|
}
|
|
}
|
|
@@ -6771,6 +6820,8 @@ private extension ViewController {
|
|
|
renderScheduleCards(into: stack, meetings: [])
|
|
renderScheduleCards(into: stack, meetings: [])
|
|
|
}
|
|
}
|
|
|
scheduleCachedMeetings = []
|
|
scheduleCachedMeetings = []
|
|
|
|
|
+ publishWidgetMeetingsSnapshot(from: [])
|
|
|
|
|
+ DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
|
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
|
applySchedulePageFiltersAndRender()
|
|
applySchedulePageFiltersAndRender()
|
|
|
if calendarPageGridStack != nil {
|
|
if calendarPageGridStack != nil {
|
|
@@ -6783,6 +6834,26 @@ private extension ViewController {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func publishWidgetMeetingsSnapshot(from meetings: [ScheduledMeeting]) {
|
|
|
|
|
+ let formatter = DateFormatter()
|
|
|
|
|
+ formatter.dateFormat = "EEE, h:mm a"
|
|
|
|
|
+ formatter.locale = Locale.current
|
|
|
|
|
+ let payload = meetings.prefix(widgetSnapshotLimit).map { meeting in
|
|
|
|
|
+ let endText = DateFormatter.localizedString(from: meeting.endDate, dateStyle: .none, timeStyle: .short)
|
|
|
|
|
+ return WidgetMeetingSnapshot(
|
|
|
|
|
+ id: meeting.id,
|
|
|
|
|
+ title: meeting.title,
|
|
|
|
|
+ timeText: "\(formatter.string(from: meeting.startDate)) - \(endText)",
|
|
|
|
|
+ joinLink: meeting.meetURL.absoluteString
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ if let data = try? JSONEncoder().encode(payload) {
|
|
|
|
|
+ UserDefaults.standard.set(data, forKey: WidgetMeetingStore.key)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ UserDefaults.standard.removeObject(forKey: WidgetMeetingStore.key)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
func showScheduleHelp() {
|
|
func showScheduleHelp() {
|
|
|
let alert = NSAlert()
|
|
let alert = NSAlert()
|
|
|
alert.messageText = "Google schedule"
|
|
alert.messageText = "Google schedule"
|
|
@@ -6801,6 +6872,7 @@ private extension ViewController {
|
|
|
self.scheduleDateHeadingLabel?.stringValue = "Refreshing…"
|
|
self.scheduleDateHeadingLabel?.stringValue = "Refreshing…"
|
|
|
self.pageCache[.joinMeetings] = nil
|
|
self.pageCache[.joinMeetings] = nil
|
|
|
self.pageCache[.photo] = nil
|
|
self.pageCache[.photo] = nil
|
|
|
|
|
+ self.pageCache[.widgets] = nil
|
|
|
self.showSidebarPage(self.selectedSidebarPage)
|
|
self.showSidebarPage(self.selectedSidebarPage)
|
|
|
}
|
|
}
|
|
|
await self.loadSchedule()
|
|
await self.loadSchedule()
|
|
@@ -6830,6 +6902,7 @@ private extension ViewController {
|
|
|
self.pageCache[.joinMeetings] = nil
|
|
self.pageCache[.joinMeetings] = nil
|
|
|
self.pageCache[.photo] = nil
|
|
self.pageCache[.photo] = nil
|
|
|
self.pageCache[.video] = nil
|
|
self.pageCache[.video] = nil
|
|
|
|
|
+ self.pageCache[.widgets] = nil
|
|
|
self.pageCache[.settings] = nil
|
|
self.pageCache[.settings] = nil
|
|
|
self.showSidebarPage(self.selectedSidebarPage)
|
|
self.showSidebarPage(self.selectedSidebarPage)
|
|
|
}
|
|
}
|
|
@@ -6878,10 +6951,13 @@ private extension ViewController {
|
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
MeetingReminderManager.shared.cancelAllReminders()
|
|
|
try googleOAuth.signOut()
|
|
try googleOAuth.signOut()
|
|
|
applyGoogleProfile(nil)
|
|
applyGoogleProfile(nil)
|
|
|
|
|
+ publishWidgetMeetingsSnapshot(from: [])
|
|
|
|
|
+ DesktopWidgetWindowManager.shared.refreshForAuthStateChange()
|
|
|
updateGoogleAuthButtonTitle()
|
|
updateGoogleAuthButtonTitle()
|
|
|
pageCache[.joinMeetings] = nil
|
|
pageCache[.joinMeetings] = nil
|
|
|
pageCache[.photo] = nil
|
|
pageCache[.photo] = nil
|
|
|
pageCache[.video] = nil
|
|
pageCache[.video] = nil
|
|
|
|
|
+ pageCache[.widgets] = nil
|
|
|
pageCache[.settings] = nil
|
|
pageCache[.settings] = nil
|
|
|
showSidebarPage(selectedSidebarPage)
|
|
showSidebarPage(selectedSidebarPage)
|
|
|
Task { [weak self] in
|
|
Task { [weak self] in
|