|
|
@@ -37,9 +37,17 @@ class ViewController: NSViewController {
|
|
|
private weak var emptyMeetingLabel: NSTextField?
|
|
|
private weak var meetingsListStack: NSStackView?
|
|
|
private weak var meetingsStatusLabel: NSTextField?
|
|
|
+ private weak var meetingsScrollView: NSScrollView?
|
|
|
private var clockTimer: Timer?
|
|
|
+ private var meetingsRefreshTimer: Timer?
|
|
|
private var isSigningIn = false
|
|
|
private var isPromptingZoomCredentials = false
|
|
|
+ private var isLoadingMeetings = false
|
|
|
+ private var meetingsScrollObserver: NSObjectProtocol?
|
|
|
+ private var lastMeetingsRefreshAt = Date.distantPast
|
|
|
+ private var lastScrollEdgeRefreshAt = Date.distantPast
|
|
|
+ private let meetingsRefreshInterval: TimeInterval = 8
|
|
|
+ private let scrollRefreshCooldown: TimeInterval = 3
|
|
|
|
|
|
override func viewDidLoad() {
|
|
|
super.viewDidLoad()
|
|
|
@@ -73,6 +81,9 @@ class ViewController: NSViewController {
|
|
|
|
|
|
private func showLoginView() {
|
|
|
clockTimer?.invalidate()
|
|
|
+ meetingsRefreshTimer?.invalidate()
|
|
|
+ meetingsRefreshTimer = nil
|
|
|
+ clearMeetingsScrollObserver()
|
|
|
homeView?.removeFromSuperview()
|
|
|
homeView = nil
|
|
|
isSigningIn = false
|
|
|
@@ -89,12 +100,14 @@ class ViewController: NSViewController {
|
|
|
|
|
|
private func showHomeView(profile: GoogleUserProfile?) {
|
|
|
loginView?.removeFromSuperview()
|
|
|
+ clearMeetingsScrollObserver()
|
|
|
homeView?.removeFromSuperview()
|
|
|
homeView = makeHomeView(profile: profile)
|
|
|
if let homeView { attachToRoot(homeView) }
|
|
|
persistLoggedInState(true)
|
|
|
startClock()
|
|
|
- Task { await loadScheduledMeetings() }
|
|
|
+ startMeetingsAutoRefresh()
|
|
|
+ triggerMeetingsRefresh(force: true)
|
|
|
}
|
|
|
|
|
|
private func isUserLoggedIn() -> Bool {
|
|
|
@@ -188,12 +201,81 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
|
|
|
@objc private func logoutTapped() {
|
|
|
+ meetingsRefreshTimer?.invalidate()
|
|
|
+ meetingsRefreshTimer = nil
|
|
|
+ clearMeetingsScrollObserver()
|
|
|
googleOAuth.clearSavedTokens()
|
|
|
zoomOAuth.clearSavedTokens()
|
|
|
persistLoggedInState(false)
|
|
|
showLoginView()
|
|
|
}
|
|
|
|
|
|
+ private func startMeetingsAutoRefresh() {
|
|
|
+ meetingsRefreshTimer?.invalidate()
|
|
|
+ // Poll Zoom meetings periodically so newly scheduled meetings appear automatically.
|
|
|
+ meetingsRefreshTimer = Timer.scheduledTimer(withTimeInterval: meetingsRefreshInterval, repeats: true) { [weak self] _ in
|
|
|
+ guard let self else { return }
|
|
|
+ self.triggerMeetingsRefresh()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func triggerMeetingsRefresh(force: Bool = false) {
|
|
|
+ let now = Date()
|
|
|
+ if force == false, now.timeIntervalSince(lastMeetingsRefreshAt) < meetingsRefreshInterval {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ lastMeetingsRefreshAt = now
|
|
|
+ Task { await self.loadScheduledMeetings() }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func clearMeetingsScrollObserver() {
|
|
|
+ if let meetingsScrollObserver {
|
|
|
+ NotificationCenter.default.removeObserver(meetingsScrollObserver)
|
|
|
+ }
|
|
|
+ meetingsScrollObserver = nil
|
|
|
+ meetingsScrollView?.contentView.postsBoundsChangedNotifications = false
|
|
|
+ meetingsScrollView = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ private func observeMeetingsScrollEdges(in scrollView: NSScrollView) {
|
|
|
+ clearMeetingsScrollObserver()
|
|
|
+ meetingsScrollView = scrollView
|
|
|
+ scrollView.contentView.postsBoundsChangedNotifications = true
|
|
|
+ meetingsScrollObserver = NotificationCenter.default.addObserver(
|
|
|
+ forName: NSView.boundsDidChangeNotification,
|
|
|
+ object: scrollView.contentView,
|
|
|
+ queue: .main
|
|
|
+ ) { [weak self, weak scrollView] _ in
|
|
|
+ guard let self, let scrollView else { return }
|
|
|
+ self.refreshMeetingsIfScrolledToEdge(scrollView)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func refreshMeetingsIfScrolledToEdge(_ scrollView: NSScrollView) {
|
|
|
+ guard let documentView = scrollView.documentView else { return }
|
|
|
+
|
|
|
+ let visibleRect = scrollView.contentView.bounds
|
|
|
+ let contentHeight = documentView.bounds.height
|
|
|
+ let viewportHeight = visibleRect.height
|
|
|
+ if contentHeight <= viewportHeight + 2 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ let maxOffset = max(contentHeight - viewportHeight, 0)
|
|
|
+ let y = visibleRect.origin.y
|
|
|
+ let threshold: CGFloat = 12
|
|
|
+ let reachedTop = y <= threshold
|
|
|
+ let reachedBottom = y >= (maxOffset - threshold)
|
|
|
+ guard reachedTop || reachedBottom else { return }
|
|
|
+
|
|
|
+ let now = Date()
|
|
|
+ if now.timeIntervalSince(lastScrollEdgeRefreshAt) < scrollRefreshCooldown {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ lastScrollEdgeRefreshAt = now
|
|
|
+ triggerMeetingsRefresh(force: true)
|
|
|
+ }
|
|
|
+
|
|
|
@MainActor
|
|
|
private func resetLoginSigningInState() {
|
|
|
isSigningIn = false
|
|
|
@@ -251,6 +333,9 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
|
|
|
private func loadScheduledMeetings() async {
|
|
|
+ if isLoadingMeetings { return }
|
|
|
+ isLoadingMeetings = true
|
|
|
+ defer { isLoadingMeetings = false }
|
|
|
do {
|
|
|
let zoomToken = try await zoomOAuth.validAccessToken(presentingWindow: view.window)
|
|
|
let zoomMeetings = try await fetchZoomScheduledMeetings(accessToken: zoomToken)
|
|
|
@@ -729,6 +814,7 @@ class ViewController: NSViewController {
|
|
|
meetingsListStack = meetingsStack
|
|
|
meetingsStatusLabel = meetingsStatus
|
|
|
emptyMeetingLabel = noMeeting
|
|
|
+ observeMeetingsScrollEdges(in: meetingsScrollView)
|
|
|
updateClock()
|
|
|
return root
|
|
|
}
|