Bladeren bron

Handle rate limit and settings navigation

Slow meetings polling and back off on Zoom 429s with a friendly status message; make Settings selectable with a stable gear icon.

Made-with: Cursor
huzaifahayat12 5 dagen geleden
bovenliggende
commit
ef9a36bc96
1 gewijzigde bestanden met toevoegingen van 53 en 8 verwijderingen
  1. 53 8
      zoom_app/ViewController.swift

+ 53 - 8
zoom_app/ViewController.swift

@@ -79,8 +79,10 @@ class ViewController: NSViewController {
79 79
     private var meetingsScrollObserver: NSObjectProtocol?
80 80
     private var lastMeetingsRefreshAt = Date.distantPast
81 81
     private var lastScrollEdgeRefreshAt = Date.distantPast
82
-    private let meetingsRefreshInterval: TimeInterval = 8
82
+    // Keep this conservative to avoid Zoom API rate limits.
83
+    private let meetingsRefreshInterval: TimeInterval = 60
83 84
     private let scrollRefreshCooldown: TimeInterval = 3
85
+    private var meetingsRateLimitedUntil: Date?
84 86
     
85 87
     private enum SidebarStyle {
86 88
         case login
@@ -321,6 +323,9 @@ class ViewController: NSViewController {
321 323
 
322 324
     private func triggerMeetingsRefresh(force: Bool = false) {
323 325
         let now = Date()
326
+        if let until = meetingsRateLimitedUntil, now < until {
327
+            return
328
+        }
324 329
         if force == false, now.timeIntervalSince(lastMeetingsRefreshAt) < meetingsRefreshInterval {
325 330
             return
326 331
         }
@@ -604,6 +609,7 @@ class ViewController: NSViewController {
604 609
             let zoomToken = try await zoomOAuth.validAccessToken(presentingWindow: view.window)
605 610
             let zoomMeetings = try await fetchZoomScheduledMeetings(accessToken: zoomToken)
606 611
             await MainActor.run {
612
+                self.meetingsRateLimitedUntil = nil
607 613
                 self.applyMeetings(zoomMeetings)
608 614
             }
609 615
         } catch {
@@ -617,9 +623,14 @@ class ViewController: NSViewController {
617 623
                     self.promptForZoomOAuthCredentialsIfNeeded()
618 624
                 } else if case ZoomOAuthError.missingRequiredScope(let scopeMessage) = error {
619 625
                     self.zoomOAuth.clearSavedTokens()
620
-                    self.meetingsStatusLabel?.stringValue = "Zoom OAuth scope missing. Add required scopes in Marketplace, click Add app now, then sign in again. (\(scopeMessage))"
626
+                    self.meetingsStatusLabel?.stringValue = "Zoom permissions are missing. Update your Zoom app scopes, then sign in again."
627
+                } else if case ZoomOAuthError.rateLimited(let retryAfterSeconds) = error {
628
+                    let seconds = max(retryAfterSeconds ?? 300, 30)
629
+                    self.meetingsRateLimitedUntil = Date().addingTimeInterval(TimeInterval(seconds))
630
+                    let minutes = Int(ceil(Double(seconds) / 60.0))
631
+                    self.meetingsStatusLabel?.stringValue = "Zoom rate limit reached. Please try again in \(minutes) min."
621 632
                 } else {
622
-                    self.meetingsStatusLabel?.stringValue = "Zoom API error: \(error.localizedDescription)"
633
+                    self.meetingsStatusLabel?.stringValue = "Unable to load meetings right now. Please try again shortly."
623 634
                 }
624 635
             }
625 636
         }
@@ -800,6 +811,11 @@ class ViewController: NSViewController {
800 811
             let (data, response) = try await URLSession.shared.data(for: request)
801 812
             guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
802 813
                 let raw = String(data: data, encoding: .utf8) ?? "Failed to load Zoom meetings"
814
+                if (response as? HTTPURLResponse)?.statusCode == 429 {
815
+                    let retryAfterRaw = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "Retry-After")
816
+                    let seconds = retryAfterRaw.flatMap { Int($0.trimmingCharacters(in: .whitespacesAndNewlines)) }
817
+                    throw ZoomOAuthError.rateLimited(retryAfterSeconds: seconds)
818
+                }
803 819
                 if raw.localizedCaseInsensitiveContains("does not contain scopes") {
804 820
                     throw ZoomOAuthError.missingRequiredScope(raw)
805 821
                 }
@@ -1478,7 +1494,8 @@ class ViewController: NSViewController {
1478 1494
             let settingsRow = NSView()
1479 1495
             settingsRow.translatesAutoresizingMaskIntoConstraints = false
1480 1496
             settingsRow.wantsLayer = true
1481
-            settingsRow.layer?.backgroundColor = NSColor.clear.cgColor
1497
+            let settingsSelected = selected == "Settings"
1498
+            settingsRow.layer?.backgroundColor = settingsSelected ? sidebarActiveBackground.withAlphaComponent(0.95).cgColor : NSColor.clear.cgColor
1482 1499
             settingsRow.layer?.cornerRadius = 12
1483 1500
             settingsRow.widthAnchor.constraint(equalToConstant: 68).isActive = true
1484 1501
             settingsRow.heightAnchor.constraint(equalToConstant: 66).isActive = true
@@ -1489,16 +1506,26 @@ class ViewController: NSViewController {
1489 1506
 
1490 1507
             let iconView = NSImageView()
1491 1508
             iconView.translatesAutoresizingMaskIntoConstraints = false
1492
-            iconView.contentTintColor = primaryText
1509
+            iconView.contentTintColor = settingsSelected ? primaryText : secondaryText
1493 1510
             iconView.imageScaling = .scaleProportionallyUpOrDown
1494 1511
             iconView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 20, weight: .regular)
1495
-            iconView.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings")
1512
+            let settingsSymbolPreferred = settingsSelected ? "gearshape.fill" : "gearshape"
1513
+            iconView.image = NSImage(systemSymbolName: settingsSymbolPreferred, accessibilityDescription: "Settings")
1514
+                ?? NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings")
1496 1515
             iconContainer.addSubview(iconView)
1497 1516
 
1498
-            let label = makeLabel("Settings", size: 10, color: secondaryText, weight: .regular, centered: true)
1517
+            let label = makeLabel("Settings", size: 10, color: settingsSelected ? primaryText : secondaryText, weight: .regular, centered: true)
1499 1518
             label.translatesAutoresizingMaskIntoConstraints = false
1500 1519
             settingsRow.addSubview(label)
1501 1520
 
1521
+            let hit = NSButton(title: "", target: self, action: #selector(homeSidebarItemTapped(_:)))
1522
+            hit.identifier = NSUserInterfaceItemIdentifier("Settings")
1523
+            hit.isBordered = false
1524
+            hit.bezelStyle = .shadowlessSquare
1525
+            hit.focusRingType = .none
1526
+            hit.translatesAutoresizingMaskIntoConstraints = false
1527
+            settingsRow.addSubview(hit, positioned: .above, relativeTo: nil)
1528
+
1502 1529
             NSLayoutConstraint.activate([
1503 1530
                 iconContainer.topAnchor.constraint(equalTo: settingsRow.topAnchor, constant: 9),
1504 1531
                 iconContainer.centerXAnchor.constraint(equalTo: settingsRow.centerXAnchor),
@@ -1512,9 +1539,18 @@ class ViewController: NSViewController {
1512 1539
 
1513 1540
                 label.topAnchor.constraint(equalTo: iconContainer.bottomAnchor, constant: 6),
1514 1541
                 label.centerXAnchor.constraint(equalTo: settingsRow.centerXAnchor),
1515
-                label.bottomAnchor.constraint(equalTo: settingsRow.bottomAnchor, constant: -8)
1542
+                label.bottomAnchor.constraint(equalTo: settingsRow.bottomAnchor, constant: -8),
1543
+
1544
+                hit.leadingAnchor.constraint(equalTo: settingsRow.leadingAnchor),
1545
+                hit.trailingAnchor.constraint(equalTo: settingsRow.trailingAnchor),
1546
+                hit.topAnchor.constraint(equalTo: settingsRow.topAnchor),
1547
+                hit.bottomAnchor.constraint(equalTo: settingsRow.bottomAnchor)
1516 1548
             ])
1517 1549
 
1550
+            homeSidebarRowViews["Settings"] = settingsRow
1551
+            homeSidebarIconViews["Settings"] = iconView
1552
+            homeSidebarLabels["Settings"] = label
1553
+
1518 1554
             stack.addArrangedSubview(settingsRow)
1519 1555
         }
1520 1556
 
@@ -1598,6 +1634,9 @@ class ViewController: NSViewController {
1598 1634
         case "Scheduler":
1599 1635
             // `calendar.badge.clock.fill` is not available on macOS; keep a stable symbol.
1600 1636
             return "calendar.badge.clock"
1637
+        case "Settings":
1638
+            // `gearshape.fill` may not exist on all macOS versions; handled via safe image assignment.
1639
+            return filled ? "gearshape.fill" : "gearshape"
1601 1640
         case "Hub":
1602 1641
             return "square.grid.3x3"
1603 1642
         case "More":
@@ -1978,6 +2017,7 @@ enum ZoomOAuthError: Error {
1978 2017
     case missingAuthorizationCode
1979 2018
     case tokenExchangeFailed(String)
1980 2019
     case missingRequiredScope(String)
2020
+    case rateLimited(retryAfterSeconds: Int?)
1981 2021
     case unableToOpenBrowser
1982 2022
     case authenticationTimedOut
1983 2023
 }
@@ -2556,6 +2596,11 @@ extension ZoomOAuthError: LocalizedError {
2556 2596
             return details
2557 2597
         case .missingRequiredScope(let details):
2558 2598
             return "The Zoom access token is missing required scopes. \(details)"
2599
+        case .rateLimited(let retryAfterSeconds):
2600
+            if let retryAfterSeconds {
2601
+                return "Zoom rate limit reached. Try again in \(retryAfterSeconds) seconds."
2602
+            }
2603
+            return "Zoom rate limit reached. Try again later."
2559 2604
         case .unableToOpenBrowser:
2560 2605
             return "Could not open the system browser for Zoom sign-in."
2561 2606
         case .authenticationTimedOut: