Browse Source

Build out the Schedule page with advanced filtering and paged card browsing.

This replaces the placeholder with a dedicated schedule experience, adds custom date-range filtering, renders cards in a 3-column scrollable layout, and keeps refresh/filter interactions aligned with the current page state.

Made-with: Cursor
huzaifahayat12 3 tháng trước cách đây
mục cha
commit
5968d21f39
1 tập tin đã thay đổi với 423 bổ sung3 xóa
  1. 423 3
      meetings_app/ViewController.swift

+ 423 - 3
meetings_app/ViewController.swift

@@ -292,6 +292,14 @@ final class ViewController: NSViewController {
         case week = 2
     }
 
+    private enum SchedulePageFilter: Int {
+        case all = 0
+        case today = 1
+        case week = 2
+        case month = 3
+        case customRange = 4
+    }
+
     private var scheduleFilter: ScheduleFilter = .all
     private weak var scheduleDateHeadingLabel: NSTextField?
     private weak var scheduleCardsStack: NSStackView?
@@ -313,6 +321,25 @@ final class ViewController: NSViewController {
     private var scheduleProfileMenuAvatar: NSImage?
     private var scheduleProfileImageTask: Task<Void, Never>?
     private var googleAccountPopover: NSPopover?
+    private var scheduleCachedMeetings: [ScheduledMeeting] = []
+
+    private var schedulePageFilter: SchedulePageFilter = .all
+    private var schedulePageFromDate: Date = Calendar.current.startOfDay(for: Date())
+    private var schedulePageToDate: Date = Calendar.current.startOfDay(for: Date())
+    private var schedulePageFilteredMeetings: [ScheduledMeeting] = []
+    private var schedulePageVisibleCount: Int = 0
+    private let schedulePageBatchSize: Int = 20
+    private let schedulePageCardsPerRow: Int = 3
+    private let schedulePageCardWidth: CGFloat = 240
+    private let schedulePageCardSpacing: CGFloat = 12
+    private var schedulePageScrollObservation: NSObjectProtocol?
+    private weak var schedulePageDateHeadingLabel: NSTextField?
+    private weak var schedulePageFilterDropdown: NSPopUpButton?
+    private weak var schedulePageFromDatePicker: NSDatePicker?
+    private weak var schedulePageToDatePicker: NSDatePicker?
+    private weak var schedulePageRangeErrorLabel: NSTextField?
+    private weak var schedulePageCardsStack: NSStackView?
+    private weak var schedulePageCardsScrollView: NSScrollView?
 
     /// In-app browser navigation: `.allowAll` or `.whitelist(hostSuffixes:)` (e.g. `["google.com"]` matches `meet.google.com`).
     private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
@@ -413,6 +440,9 @@ final class ViewController: NSViewController {
         if hasObservedAppLifecycleForUsage {
             NotificationCenter.default.removeObserver(self)
         }
+        if let observer = schedulePageScrollObservation {
+            NotificationCenter.default.removeObserver(observer)
+        }
         storeKitStartupTask?.cancel()
         paywallPurchaseTask?.cancel()
         launchPaywallWorkItem?.cancel()
@@ -735,6 +765,10 @@ private extension ViewController {
 
     private func reloadTheme() {
         pageCache.removeAll()
+        if let observer = schedulePageScrollObservation {
+            NotificationCenter.default.removeObserver(observer)
+        }
+        schedulePageScrollObservation = nil
         sidebarRowViews.removeAll()
         sidebarPageByView.removeAll()
         zoomJoinModeByView.removeAll()
@@ -1293,7 +1327,7 @@ private extension ViewController {
         case .joinMeetings:
             built = makeJoinMeetingsContent()
         case .photo:
-            built = makePlaceholderPage(title: "Schedule", subtitle: "Create and manage your upcoming meetings.")
+            built = makeSchedulePageContent()
         case .video:
             built = makePlaceholderPage(title: "Calendar", subtitle: "View meetings by date and track your plan.")
         case .settings:
@@ -1699,6 +1733,47 @@ private extension ViewController {
         return panel
     }
 
+    func makeSchedulePageContent() -> NSView {
+        let panel = NSView()
+        panel.translatesAutoresizingMaskIntoConstraints = false
+
+        let contentStack = NSStackView()
+        contentStack.translatesAutoresizingMaskIntoConstraints = false
+        contentStack.orientation = .vertical
+        contentStack.spacing = 12
+        contentStack.alignment = .leading
+
+        contentStack.addArrangedSubview(schedulePageHeader())
+
+        let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
+        schedulePageDateHeadingLabel = heading
+        contentStack.addArrangedSubview(heading)
+
+        let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
+        rangeError.isHidden = true
+        schedulePageRangeErrorLabel = rangeError
+        contentStack.addArrangedSubview(rangeError)
+
+        let cardsContainer = makeSchedulePageCardsContainer()
+        contentStack.addArrangedSubview(cardsContainer)
+
+        panel.addSubview(contentStack)
+
+        NSLayoutConstraint.activate([
+            contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
+            contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
+            contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 6),
+            cardsContainer.leadingAnchor.constraint(equalTo: contentStack.leadingAnchor),
+            cardsContainer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -16)
+        ])
+
+        Task { [weak self] in
+            await self?.loadSchedule()
+        }
+
+        return panel
+    }
+
     func meetJoinSectionRow() -> NSView {
         let row = NSStackView()
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -2580,6 +2655,163 @@ private extension ViewController {
         return row
     }
 
+    private func schedulePageHeader() -> NSView {
+        let container = NSStackView()
+        container.translatesAutoresizingMaskIntoConstraints = false
+        container.orientation = .vertical
+        container.spacing = 10
+        container.alignment = .leading
+
+        let titleLabel = textLabel("Schedule", font: typography.pageTitle, color: palette.textPrimary)
+        container.addArrangedSubview(titleLabel)
+
+        let filterRow = NSStackView()
+        filterRow.translatesAutoresizingMaskIntoConstraints = false
+        filterRow.orientation = .horizontal
+        filterRow.alignment = .centerY
+        filterRow.spacing = 10
+
+        let filterDropdown = makeSchedulePageFilterDropdown()
+        schedulePageFilterDropdown = filterDropdown
+        filterRow.addArrangedSubview(filterDropdown)
+
+        let fromPicker = makeScheduleDatePicker(date: schedulePageFromDate)
+        schedulePageFromDatePicker = fromPicker
+        filterRow.addArrangedSubview(fromPicker)
+
+        let toPicker = makeScheduleDatePicker(date: schedulePageToDate)
+        schedulePageToDatePicker = toPicker
+        filterRow.addArrangedSubview(toPicker)
+
+        filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Apply", action: #selector(schedulePageApplyDateRangePressed(_:))))
+        filterRow.addArrangedSubview(makeSchedulePagePillButton(title: "Reset", action: #selector(schedulePageResetFiltersPressed(_:))))
+        filterRow.addArrangedSubview(makeScheduleRefreshButton())
+
+        container.addArrangedSubview(filterRow)
+        container.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
+        refreshSchedulePageDateFilterUI()
+        return container
+    }
+
+    private func makeSchedulePageFilterDropdown() -> NSPopUpButton {
+        let button = HoverPopUpButton(frame: .zero, pullsDown: false)
+        button.translatesAutoresizingMaskIntoConstraints = false
+        button.autoenablesItems = false
+        button.isBordered = false
+        button.bezelStyle = .regularSquare
+        button.wantsLayer = true
+        button.layer?.cornerRadius = 8
+        button.layer?.masksToBounds = true
+        button.layer?.backgroundColor = palette.inputBackground.cgColor
+        button.layer?.borderColor = palette.inputBorder.cgColor
+        button.layer?.borderWidth = 1
+        button.font = typography.filterText
+        button.contentTintColor = palette.textSecondary
+        button.target = self
+        button.action = #selector(schedulePageFilterDropdownChanged(_:))
+        button.heightAnchor.constraint(equalToConstant: 34).isActive = true
+        button.widthAnchor.constraint(equalToConstant: 190).isActive = true
+
+        button.removeAllItems()
+        button.addItems(withTitles: ["All", "Today", "This week", "This month", "Custom range"])
+        button.selectItem(at: schedulePageFilter.rawValue)
+        if let menu = button.menu {
+            for (index, item) in menu.items.enumerated() {
+                item.tag = index
+            }
+        }
+
+        let baseColor = palette.inputBackground
+        let baseBorder = palette.inputBorder
+        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
+        let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
+        let hoverBorder = baseBorder.blended(withFraction: 0.16, of: hoverBlend) ?? baseBorder
+        button.onHoverChanged = { [weak button] hovering in
+            button?.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+            button?.layer?.borderColor = (hovering ? hoverBorder : baseBorder).cgColor
+        }
+        button.onHoverChanged?(false)
+        return button
+    }
+
+    private func makeScheduleDatePicker(date: Date) -> NSDatePicker {
+        let picker = NSDatePicker()
+        picker.translatesAutoresizingMaskIntoConstraints = false
+        picker.datePickerStyle = .textFieldAndStepper
+        picker.datePickerElements = [.yearMonthDay]
+        picker.dateValue = date
+        picker.font = typography.filterText
+        picker.heightAnchor.constraint(equalToConstant: 34).isActive = true
+        picker.widthAnchor.constraint(equalToConstant: 156).isActive = true
+        picker.target = self
+        picker.action = #selector(schedulePageDatePickerChanged(_:))
+        return picker
+    }
+
+    private func makeSchedulePagePillButton(title: String, action: Selector) -> NSButton {
+        let button = makeSchedulePillButton(title: title)
+        button.target = self
+        button.action = action
+        button.widthAnchor.constraint(equalToConstant: 92).isActive = true
+        return button
+    }
+
+    private func makeSchedulePageCardsContainer() -> NSView {
+        if let observer = schedulePageScrollObservation {
+            NotificationCenter.default.removeObserver(observer)
+        }
+        schedulePageScrollObservation = nil
+
+        let wrapper = NSView()
+        wrapper.translatesAutoresizingMaskIntoConstraints = false
+
+        let scroll = NSScrollView()
+        scroll.translatesAutoresizingMaskIntoConstraints = false
+        scroll.drawsBackground = false
+        scroll.hasHorizontalScroller = false
+        scroll.hasVerticalScroller = true
+        scroll.autohidesScrollers = true
+        scroll.borderType = .noBorder
+        let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
+        scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
+        schedulePageCardsScrollView = scroll
+        wrapper.addSubview(scroll)
+
+        let stack = NSStackView()
+        stack.translatesAutoresizingMaskIntoConstraints = false
+        stack.orientation = .vertical
+        stack.spacing = 12
+        stack.alignment = .leading
+        schedulePageCardsStack = stack
+        scroll.documentView = stack
+
+        NSLayoutConstraint.activate([
+            scroll.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
+            scroll.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
+            scroll.topAnchor.constraint(equalTo: wrapper.topAnchor),
+            scroll.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
+            scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 420),
+
+            stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
+            stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
+            stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
+            stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
+            stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
+        ])
+
+        scroll.contentView.postsBoundsChangedNotifications = true
+        schedulePageScrollObservation = NotificationCenter.default.addObserver(
+            forName: NSView.boundsDidChangeNotification,
+            object: scroll.contentView,
+            queue: .main
+        ) { [weak self] _ in
+            self?.schedulePageScrolled()
+        }
+
+        renderSchedulePageCards()
+        return wrapper
+    }
+
     private func scheduleTopAuthRow() -> NSView {
         let row = NSStackView()
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -3960,6 +4192,10 @@ private extension ViewController {
         googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "Loading…"
     }
 
+    private func schedulePageInitialHeadingText() -> String {
+        googleOAuth.loadTokens() == nil ? "Connect Google to see meetings" : "Loading schedule…"
+    }
+
     @objc func scheduleFilterDropdownChanged(_ sender: NSPopUpButton) {
         guard let selectedItem = sender.selectedItem,
               let filter = ScheduleFilter(rawValue: selectedItem.tag) else { return }
@@ -3974,6 +4210,38 @@ private extension ViewController {
         }
     }
 
+    @objc func schedulePageFilterDropdownChanged(_ sender: NSPopUpButton) {
+        guard let selectedItem = sender.selectedItem,
+              let filter = SchedulePageFilter(rawValue: selectedItem.tag) else { return }
+        schedulePageFilter = filter
+        refreshSchedulePageDateFilterUI()
+        applySchedulePageFiltersAndRender()
+    }
+
+    @objc func schedulePageDatePickerChanged(_ sender: NSDatePicker) {
+        schedulePageFromDate = schedulePageFromDatePicker?.dateValue ?? schedulePageFromDate
+        schedulePageToDate = schedulePageToDatePicker?.dateValue ?? schedulePageToDate
+    }
+
+    @objc func schedulePageApplyDateRangePressed(_ sender: NSButton) {
+        schedulePageFilter = .customRange
+        schedulePageFilterDropdown?.selectItem(at: SchedulePageFilter.customRange.rawValue)
+        refreshSchedulePageDateFilterUI()
+        applySchedulePageFiltersAndRender()
+    }
+
+    @objc func schedulePageResetFiltersPressed(_ sender: NSButton) {
+        schedulePageFilter = .all
+        schedulePageFilterDropdown?.selectItem(at: SchedulePageFilter.all.rawValue)
+        let today = Calendar.current.startOfDay(for: Date())
+        schedulePageFromDate = today
+        schedulePageToDate = today
+        schedulePageFromDatePicker?.dateValue = today
+        schedulePageToDatePicker?.dateValue = today
+        refreshSchedulePageDateFilterUI()
+        applySchedulePageFiltersAndRender()
+    }
+
     private func scheduleTimeText(for meeting: ScheduledMeeting) -> String {
         if meeting.isAllDay { return "All day" }
         let f = DateFormatter()
@@ -4073,6 +4341,148 @@ private extension ViewController {
         }
     }
 
+    private func filteredMeetingsForSchedulePage(_ meetings: [ScheduledMeeting]) -> [ScheduledMeeting] {
+        let calendar = Calendar.current
+        switch schedulePageFilter {
+        case .all:
+            return meetings
+        case .today:
+            let start = calendar.startOfDay(for: Date())
+            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:
+            let start = calendar.startOfDay(for: schedulePageFromDate)
+            let inclusiveEndDay = calendar.startOfDay(for: schedulePageToDate)
+            guard let end = calendar.date(byAdding: .day, value: 1, to: inclusiveEndDay) else {
+                return meetings
+            }
+            return meetings.filter { $0.startDate >= start && $0.startDate < end }
+        }
+    }
+
+    private func refreshSchedulePageDateFilterUI() {
+        let isCustom = schedulePageFilter == .customRange
+        schedulePageFromDatePicker?.isEnabled = isCustom
+        schedulePageToDatePicker?.isEnabled = isCustom
+        schedulePageFromDatePicker?.alphaValue = isCustom ? 1.0 : 0.65
+        schedulePageToDatePicker?.alphaValue = isCustom ? 1.0 : 0.65
+    }
+
+    private func schedulePageHasValidCustomRange() -> Bool {
+        let start = Calendar.current.startOfDay(for: schedulePageFromDate)
+        let end = Calendar.current.startOfDay(for: schedulePageToDate)
+        return start <= end
+    }
+
+    private func setSchedulePageRangeError(_ message: String?) {
+        guard let label = schedulePageRangeErrorLabel else { return }
+        label.stringValue = message ?? ""
+        label.isHidden = message == nil
+    }
+
+    private func applySchedulePageFiltersAndRender() {
+        schedulePageFromDate = schedulePageFromDatePicker?.dateValue ?? schedulePageFromDate
+        schedulePageToDate = schedulePageToDatePicker?.dateValue ?? schedulePageToDate
+        if schedulePageFilter == .customRange && !schedulePageHasValidCustomRange() {
+            setSchedulePageRangeError("Start date must be on or before end date.")
+            schedulePageFilteredMeetings = []
+            schedulePageVisibleCount = 0
+            renderSchedulePageCards()
+            schedulePageDateHeadingLabel?.stringValue = "Invalid custom date range"
+            return
+        }
+
+        setSchedulePageRangeError(nil)
+        schedulePageFilteredMeetings = filteredMeetingsForSchedulePage(scheduleCachedMeetings)
+        schedulePageVisibleCount = min(schedulePageBatchSize, schedulePageFilteredMeetings.count)
+        renderSchedulePageCards()
+        schedulePageDateHeadingLabel?.stringValue = scheduleHeadingText(for: schedulePageFilteredMeetings)
+    }
+
+    private func appendSchedulePageBatchIfNeeded() {
+        guard schedulePageVisibleCount < schedulePageFilteredMeetings.count else { return }
+        let nextCount = min(schedulePageVisibleCount + schedulePageBatchSize, schedulePageFilteredMeetings.count)
+        guard nextCount > schedulePageVisibleCount else { return }
+        schedulePageVisibleCount = nextCount
+        renderSchedulePageCards()
+    }
+
+    private func schedulePageScrolled() {
+        guard let scroll = schedulePageCardsScrollView else { return }
+        let contentBounds = scroll.contentView.bounds
+        let contentHeight = scroll.documentView?.bounds.height ?? 0
+        guard contentHeight > 0 else { return }
+        let remaining = contentHeight - (contentBounds.origin.y + contentBounds.height)
+        if remaining < 180 {
+            appendSchedulePageBatchIfNeeded()
+        }
+    }
+
+    private func renderSchedulePageCards() {
+        guard let stack = schedulePageCardsStack else { return }
+        stack.arrangedSubviews.forEach { v in
+            stack.removeArrangedSubview(v)
+            v.removeFromSuperview()
+        }
+
+        let visibleMeetings = Array(schedulePageFilteredMeetings.prefix(schedulePageVisibleCount))
+        if visibleMeetings.isEmpty {
+            let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
+            empty.translatesAutoresizingMaskIntoConstraints = false
+            empty.heightAnchor.constraint(equalToConstant: 120).isActive = true
+            let viewportWidth = (schedulePageCardWidth * CGFloat(schedulePageCardsPerRow)) + (schedulePageCardSpacing * CGFloat(schedulePageCardsPerRow - 1))
+            empty.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
+            styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+            let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
+            label.translatesAutoresizingMaskIntoConstraints = false
+            empty.addSubview(label)
+            NSLayoutConstraint.activate([
+                label.centerXAnchor.constraint(equalTo: empty.centerXAnchor),
+                label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
+            ])
+            stack.addArrangedSubview(empty)
+            return
+        }
+
+        var index = 0
+        while index < visibleMeetings.count {
+            let row = NSStackView()
+            row.translatesAutoresizingMaskIntoConstraints = false
+            row.orientation = .horizontal
+            row.alignment = .top
+            row.spacing = schedulePageCardSpacing
+            row.distribution = .fill
+            let rowEnd = min(index + schedulePageCardsPerRow, visibleMeetings.count)
+            for meeting in visibleMeetings[index..<rowEnd] {
+                row.addArrangedSubview(scheduleCard(meeting: meeting))
+            }
+            if rowEnd - index < schedulePageCardsPerRow {
+                for _ in 0..<(schedulePageCardsPerRow - (rowEnd - index)) {
+                    let spacer = NSView()
+                    spacer.translatesAutoresizingMaskIntoConstraints = false
+                    spacer.widthAnchor.constraint(equalToConstant: schedulePageCardWidth).isActive = true
+                    spacer.heightAnchor.constraint(equalToConstant: 150).isActive = true
+                    row.addArrangedSubview(spacer)
+                }
+            }
+            stack.addArrangedSubview(row)
+            index = rowEnd
+        }
+
+        if schedulePageVisibleCount < schedulePageFilteredMeetings.count {
+            let moreLabel = textLabel("Scroll to load more (\(schedulePageVisibleCount)/\(schedulePageFilteredMeetings.count))", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textMuted)
+            stack.addArrangedSubview(moreLabel)
+        }
+    }
+
     private func scrollScheduleCards(direction: Int) {
         guard let scroll = scheduleCardsScrollView else { return }
         let contentBounds = scroll.contentView.bounds
@@ -4091,9 +4501,12 @@ private extension ViewController {
                     updateGoogleAuthButtonTitle()
                     applyGoogleProfile(nil)
                     scheduleDateHeadingLabel?.stringValue = "Connect Google to see meetings"
+                    schedulePageDateHeadingLabel?.stringValue = "Connect Google to see meetings"
                     if let stack = scheduleCardsStack {
                         renderScheduleCards(into: stack, meetings: [])
                     }
+                    scheduleCachedMeetings = []
+                    applySchedulePageFiltersAndRender()
                 }
                 return
             }
@@ -4110,6 +4523,8 @@ private extension ViewController {
                 if let stack = scheduleCardsStack {
                     renderScheduleCards(into: stack, meetings: filtered)
                 }
+                scheduleCachedMeetings = meetings
+                applySchedulePageFiltersAndRender()
             }
         } catch {
             await MainActor.run {
@@ -4118,9 +4533,12 @@ private extension ViewController {
                     applyGoogleProfile(nil)
                 }
                 scheduleDateHeadingLabel?.stringValue = "Couldn’t load schedule"
+                schedulePageDateHeadingLabel?.stringValue = "Couldn’t load schedule"
                 if let stack = scheduleCardsStack {
                     renderScheduleCards(into: stack, meetings: [])
                 }
+                scheduleCachedMeetings = []
+                applySchedulePageFiltersAndRender()
                 showSimpleError("Couldn’t load schedule.", error: error)
             }
         }
@@ -4143,7 +4561,8 @@ private extension ViewController {
                 await MainActor.run {
                     self.scheduleDateHeadingLabel?.stringValue = "Refreshing…"
                     self.pageCache[.joinMeetings] = nil
-                    self.showSidebarPage(.joinMeetings)
+                    self.pageCache[.photo] = nil
+                    self.showSidebarPage(self.selectedSidebarPage)
                 }
                 await self.loadSchedule()
             } catch {
@@ -4170,7 +4589,8 @@ private extension ViewController {
                     self.updateGoogleAuthButtonTitle()
                     self.applyGoogleProfile(profile.map { self.makeGoogleProfileDisplay(from: $0) })
                     self.pageCache[.joinMeetings] = nil
-                    self.showSidebarPage(.joinMeetings)
+                    self.pageCache[.photo] = nil
+                    self.showSidebarPage(self.selectedSidebarPage)
                 }
             } catch {
                 self.showSimpleError("Couldn’t connect Google account.", error: error)