|
|
@@ -16,6 +16,32 @@ private enum AppWindowMetrics {
|
|
|
static let defaultContentHeight: CGFloat = 690
|
|
|
}
|
|
|
|
|
|
+/// OAuth entry points stay in the system browser so enterprise SSO extensions and password managers behave as users expect.
|
|
|
+private enum InAppWebBrowserPolicy {
|
|
|
+ static func shouldOpenInSystemBrowser(_ url: URL) -> Bool {
|
|
|
+ guard let host = url.host?.lowercased() else { return false }
|
|
|
+ let path = url.path.lowercased()
|
|
|
+ let zoomHost = host == "zoom.us" || host.hasSuffix(".zoom.us") || host == "zoom.com" || host.hasSuffix(".zoom.com")
|
|
|
+ if zoomHost, path.contains("/oauth/authorize") { return true }
|
|
|
+ if host == "accounts.google.com" {
|
|
|
+ return path.contains("/o/oauth2/") || path.contains("/signin/oauth")
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Errors from the main-window embedded OAuth web session (right panel).
|
|
|
+private enum EmbeddedOAuthFlowError: Error {
|
|
|
+ case webViewUnavailable
|
|
|
+ case userCancelled
|
|
|
+ case timedOut
|
|
|
+}
|
|
|
+
|
|
|
+@MainActor
|
|
|
+protocol EmbeddedOAuthPresenting: AnyObject {
|
|
|
+ func runEmbeddedOAuthSession(startURL: URL, callbackURLPrefix: String, timeoutSeconds: Double) async throws -> URL
|
|
|
+}
|
|
|
+
|
|
|
class ViewController: NSViewController {
|
|
|
enum HomeSection: String {
|
|
|
case home = "Home"
|
|
|
@@ -248,6 +274,18 @@ class ViewController: NSViewController {
|
|
|
private weak var meetingsPageEmptyLabel: NSTextField?
|
|
|
private weak var meetingsPageLoadMoreButton: NSButton?
|
|
|
private weak var meetingsPageScrollView: NSScrollView?
|
|
|
+ private weak var embeddedBrowserWebView: WKWebView?
|
|
|
+ private weak var embeddedBrowserPanel: NSView?
|
|
|
+ private weak var homeBodyRowView: NSView?
|
|
|
+ private weak var homeMainContentColumnView: NSView?
|
|
|
+ private weak var embeddedBrowserBackButton: NSButton?
|
|
|
+ private weak var embeddedBrowserForwardButton: NSButton?
|
|
|
+ private var embeddedBrowserPanelWidthConstraint: NSLayoutConstraint?
|
|
|
+ private var embeddedBrowserMainColumnCollapsedWidthConstraint: NSLayoutConstraint?
|
|
|
+ private var oauthSessionContinuation: CheckedContinuation<URL, Error>?
|
|
|
+ private var oauthSessionCallbackPrefix: String?
|
|
|
+ /// Gap between main column and right web panel (must match Auto Layout constant).
|
|
|
+ private let homeEmbeddedBrowserColumnGap: CGFloat = 10
|
|
|
private weak var homePremiumLockOverlay: NSButton?
|
|
|
private weak var meetingsPremiumLockOverlay: NSButton?
|
|
|
private weak var schedulerPremiumLockOverlay: NSButton?
|
|
|
@@ -562,6 +600,9 @@ class ViewController: NSViewController {
|
|
|
updatePaywallPanelSize()
|
|
|
updatePaywallResponsiveLayout()
|
|
|
}
|
|
|
+ if (embeddedBrowserPanelWidthConstraint?.constant ?? 0) > 0.5 {
|
|
|
+ updateEmbeddedRightPanelWidthForOpenState()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func setupUI() {
|
|
|
@@ -1519,8 +1560,14 @@ class ViewController: NSViewController {
|
|
|
return url
|
|
|
}
|
|
|
|
|
|
+ /// Opens non-sign-in web content in the system default browser.
|
|
|
+ @MainActor
|
|
|
+ private func openWebURLPreferringInApp(_ url: URL) {
|
|
|
+ _ = openURLInDefaultBrowser(url)
|
|
|
+ }
|
|
|
+
|
|
|
private func openZoomMeetingInDefaultBrowser(_ url: URL) {
|
|
|
- let opened = NSWorkspace.shared.open(url)
|
|
|
+ let opened = openURLInDefaultBrowser(url)
|
|
|
if opened {
|
|
|
joinMeetingWindow?.performClose(nil)
|
|
|
} else {
|
|
|
@@ -1528,6 +1575,129 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ @discardableResult
|
|
|
+ private func openURLInDefaultBrowser(_ url: URL) -> Bool {
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func presentURLInEmbeddedRightPanel(_ url: URL) {
|
|
|
+ guard let webView = embeddedBrowserWebView else {
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ updateEmbeddedRightPanelWidthForOpenState()
|
|
|
+ webView.load(URLRequest(url: url))
|
|
|
+ refreshEmbeddedBrowserNavButtons()
|
|
|
+ view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Makes the embedded web panel fill the entire right-side content area (to match the reference behavior).
|
|
|
+ @MainActor
|
|
|
+ private func updateEmbeddedRightPanelWidthForOpenState() {
|
|
|
+ guard let widthConstraint = embeddedBrowserPanelWidthConstraint else { return }
|
|
|
+ view.layoutSubtreeIfNeeded()
|
|
|
+ view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
|
+ var rowW = homeBodyRowView?.bounds.width ?? 0
|
|
|
+ if rowW < 1 {
|
|
|
+ rowW = view.bounds.width
|
|
|
+ }
|
|
|
+ // Fill the entire body row so the embedded web experience matches a dedicated right-side panel.
|
|
|
+ widthConstraint.constant = max(320, floor(rowW))
|
|
|
+
|
|
|
+ // Collapse the left main content column while web is visible.
|
|
|
+ homeMainContentColumnView?.isHidden = true
|
|
|
+ embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = true
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func refreshEmbeddedBrowserNavButtons() {
|
|
|
+ embeddedBrowserBackButton?.isEnabled = embeddedBrowserWebView?.canGoBack == true
|
|
|
+ embeddedBrowserForwardButton?.isEnabled = embeddedBrowserWebView?.canGoForward == true
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func embeddedBrowserBackTapped() {
|
|
|
+ embeddedBrowserWebView?.goBack()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func embeddedBrowserForwardTapped() {
|
|
|
+ embeddedBrowserWebView?.goForward()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func embeddedBrowserReloadTapped() {
|
|
|
+ embeddedBrowserWebView?.reload()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func embeddedBrowserClosePanelTapped() {
|
|
|
+ closeEmbeddedBrowserPanel()
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func closeEmbeddedBrowserPanel() {
|
|
|
+ if let cont = oauthSessionContinuation {
|
|
|
+ oauthSessionContinuation = nil
|
|
|
+ oauthSessionCallbackPrefix = nil
|
|
|
+ cont.resume(throwing: EmbeddedOAuthFlowError.userCancelled)
|
|
|
+ }
|
|
|
+ embeddedBrowserWebView?.stopLoading()
|
|
|
+ embeddedBrowserPanelWidthConstraint?.constant = 0
|
|
|
+ embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = false
|
|
|
+ homeMainContentColumnView?.isHidden = false
|
|
|
+ refreshEmbeddedBrowserNavButtons()
|
|
|
+ view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ func runEmbeddedOAuthSession(startURL: URL, callbackURLPrefix: String, timeoutSeconds: Double) async throws -> URL {
|
|
|
+ guard let webView = embeddedBrowserWebView else {
|
|
|
+ throw EmbeddedOAuthFlowError.webViewUnavailable
|
|
|
+ }
|
|
|
+ do {
|
|
|
+ return try await withThrowingTaskGroup(of: URL.self) { group in
|
|
|
+ group.addTask { @MainActor in
|
|
|
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<URL, Error>) in
|
|
|
+ self.oauthSessionContinuation = continuation
|
|
|
+ self.oauthSessionCallbackPrefix = callbackURLPrefix
|
|
|
+ self.updateEmbeddedRightPanelWidthForOpenState()
|
|
|
+ webView.load(URLRequest(url: startURL))
|
|
|
+ self.refreshEmbeddedBrowserNavButtons()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ group.addTask {
|
|
|
+ try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000))
|
|
|
+ await MainActor.run {
|
|
|
+ if let cont = self.oauthSessionContinuation {
|
|
|
+ self.oauthSessionContinuation = nil
|
|
|
+ self.oauthSessionCallbackPrefix = nil
|
|
|
+ webView.stopLoading()
|
|
|
+ cont.resume(throwing: EmbeddedOAuthFlowError.timedOut)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw EmbeddedOAuthFlowError.timedOut
|
|
|
+ }
|
|
|
+ let url = try await group.next()!
|
|
|
+ group.cancelAll()
|
|
|
+ await MainActor.run {
|
|
|
+ self.oauthSessionContinuation = nil
|
|
|
+ self.oauthSessionCallbackPrefix = nil
|
|
|
+ self.closeEmbeddedBrowserPanel()
|
|
|
+ webView.load(URLRequest(url: URL(string: "about:blank")!))
|
|
|
+ }
|
|
|
+ return url
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ await MainActor.run {
|
|
|
+ self.oauthSessionContinuation = nil
|
|
|
+ self.oauthSessionCallbackPrefix = nil
|
|
|
+ webView.stopLoading()
|
|
|
+ self.embeddedBrowserPanelWidthConstraint?.constant = 0
|
|
|
+ self.refreshEmbeddedBrowserNavButtons()
|
|
|
+ webView.load(URLRequest(url: URL(string: "about:blank")!))
|
|
|
+ self.view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
|
+ }
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private func resetJoinMeetingPanelReferences() {
|
|
|
joinMeetingWindow = nil
|
|
|
joinURLField = nil
|
|
|
@@ -1663,7 +1833,7 @@ class ViewController: NSViewController {
|
|
|
|
|
|
private func openManageSubscriptions() {
|
|
|
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
|
|
|
- NSWorkspace.shared.open(url)
|
|
|
+ openWebURLPreferringInApp(url)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1966,7 +2136,7 @@ class ViewController: NSViewController {
|
|
|
@objc private func meetingSearchResultTapped(_ sender: NSButton) {
|
|
|
guard let meeting = globalSearchResults[safe: sender.tag] else { return }
|
|
|
if let url = meeting.webURL {
|
|
|
- NSWorkspace.shared.open(url)
|
|
|
+ openWebURLPreferringInApp(url)
|
|
|
} else {
|
|
|
let pasteboard = NSPasteboard.general
|
|
|
pasteboard.clearContents()
|
|
|
@@ -5054,11 +5224,85 @@ class ViewController: NSViewController {
|
|
|
let schedulerEmptyLabel = makeLabel("No meetings scheduled for this date.", size: 15, color: secondaryText, weight: .regular, centered: true)
|
|
|
schedulerEmptyLabel.isHidden = true
|
|
|
|
|
|
- let contentColumn = NSView()
|
|
|
- contentColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ let bodyRow = NSView()
|
|
|
+ bodyRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let mainContentColumn = NSView()
|
|
|
+ mainContentColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ mainContentColumn.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
+
|
|
|
+ let rightWebPanel = NSView()
|
|
|
+ rightWebPanel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ rightWebPanel.wantsLayer = true
|
|
|
+ rightWebPanel.layer?.cornerRadius = 16
|
|
|
+ rightWebPanel.layer?.masksToBounds = true
|
|
|
+ rightWebPanel.layer?.backgroundColor = secondaryCardBackground.withAlphaComponent(0.94).cgColor
|
|
|
+ rightWebPanel.layer?.borderWidth = 1
|
|
|
+ rightWebPanel.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
+
|
|
|
+ let webToolbar = NSView()
|
|
|
+ webToolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ webToolbar.wantsLayer = true
|
|
|
+ webToolbar.layer?.backgroundColor = chromeUnifiedBackground.withAlphaComponent(0.55).cgColor
|
|
|
+
|
|
|
+ func prepareEmbeddedToolButton(_ button: NSButton, symbol: String) {
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.bezelStyle = .toolbar
|
|
|
+ if let image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil) {
|
|
|
+ button.image = image
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let browserBack = NSButton()
|
|
|
+ let browserForward = NSButton()
|
|
|
+ let browserReload = NSButton()
|
|
|
+ let browserClose = NSButton()
|
|
|
+ prepareEmbeddedToolButton(browserBack, symbol: "chevron.left")
|
|
|
+ prepareEmbeddedToolButton(browserForward, symbol: "chevron.right")
|
|
|
+ prepareEmbeddedToolButton(browserReload, symbol: "arrow.clockwise")
|
|
|
+ prepareEmbeddedToolButton(browserClose, symbol: "xmark.circle.fill")
|
|
|
+ browserClose.toolTip = "Close web panel"
|
|
|
+
|
|
|
+ browserBack.target = self
|
|
|
+ browserBack.action = #selector(embeddedBrowserBackTapped)
|
|
|
+ browserForward.target = self
|
|
|
+ browserForward.action = #selector(embeddedBrowserForwardTapped)
|
|
|
+ browserReload.target = self
|
|
|
+ browserReload.action = #selector(embeddedBrowserReloadTapped)
|
|
|
+ browserClose.target = self
|
|
|
+ browserClose.action = #selector(embeddedBrowserClosePanelTapped)
|
|
|
+
|
|
|
+ let toolbarSpring = NSView()
|
|
|
+ toolbarSpring.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ toolbarSpring.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
+
|
|
|
+ let webToolBarStack = NSStackView(views: [browserBack, browserForward, browserReload, toolbarSpring, browserClose])
|
|
|
+ webToolBarStack.orientation = .horizontal
|
|
|
+ webToolBarStack.spacing = 6
|
|
|
+ webToolBarStack.alignment = .centerY
|
|
|
+ webToolBarStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let embeddedBrowserConfiguration = WKWebViewConfiguration()
|
|
|
+ embeddedBrowserConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
+ let embeddedWK = WKWebView(frame: .zero, configuration: embeddedBrowserConfiguration)
|
|
|
+ embeddedWK.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ embeddedWK.setContentCompressionResistancePriority(.init(1), for: .horizontal)
|
|
|
+ embeddedWK.setContentHuggingPriority(.init(1), for: .horizontal)
|
|
|
+ embeddedWK.navigationDelegate = self
|
|
|
+ embeddedWK.uiDelegate = self
|
|
|
+
|
|
|
content.addSubview(topBar)
|
|
|
content.addSubview(topBarDivider)
|
|
|
- content.addSubview(contentColumn)
|
|
|
+ content.addSubview(bodyRow)
|
|
|
+ bodyRow.addSubview(mainContentColumn)
|
|
|
+ bodyRow.addSubview(rightWebPanel)
|
|
|
+ rightWebPanel.addSubview(webToolbar)
|
|
|
+ webToolbar.addSubview(webToolBarStack)
|
|
|
+ rightWebPanel.addSubview(embeddedWK)
|
|
|
+
|
|
|
+ let rightPanelWidthConstraint = rightWebPanel.widthAnchor.constraint(equalToConstant: 0)
|
|
|
+ let mainColumnCollapsedWidthConstraint = mainContentColumn.widthAnchor.constraint(equalToConstant: 0)
|
|
|
+ mainColumnCollapsedWidthConstraint.isActive = false
|
|
|
|
|
|
[brandStack, searchRow, rightTopBarCluster, searchPill, searchField, searchIcon, searchHintLabel].forEach {
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -5074,7 +5318,7 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
[timeTitle, dateTitle, actions, panel, panelHeader, meetingsStatus, meetingsDayNav, noMeeting, meetingsScrollView, refreshMeetingsButton, placeholder, settingsView, meetingsPageView, schedulerRoot].forEach {
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- contentColumn.addSubview($0)
|
|
|
+ mainContentColumn.addSubview($0)
|
|
|
}
|
|
|
[schedulerSplit, schedulerCalendarPanel, schedulerMeetingsPanel, schedulerCalendarTopRow, schedulerConnectLabel, schedulerPlusButton, schedulerMonthRow, schedulerWeekdaysRow, schedulerCalendarGrid, schedulerRightTopRow, schedulerSelectedDateLabel, schedulerTodayButton, schedulerMeetingsHeader, schedulerMeetingsScroll, schedulerEmptyLabel, schedulerMeetingsDocument, schedulerMeetingsStack].forEach {
|
|
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -5113,10 +5357,34 @@ class ViewController: NSViewController {
|
|
|
topBarDivider.trailingAnchor.constraint(equalTo: content.trailingAnchor),
|
|
|
topBarDivider.heightAnchor.constraint(equalToConstant: 1),
|
|
|
|
|
|
- contentColumn.topAnchor.constraint(equalTo: topBarDivider.bottomAnchor, constant: 14),
|
|
|
- contentColumn.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -10),
|
|
|
- contentColumn.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
|
|
|
- contentColumn.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
|
|
|
+ bodyRow.topAnchor.constraint(equalTo: topBarDivider.bottomAnchor, constant: 14),
|
|
|
+ bodyRow.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -10),
|
|
|
+ bodyRow.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 8),
|
|
|
+ bodyRow.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -8),
|
|
|
+
|
|
|
+ mainContentColumn.leadingAnchor.constraint(equalTo: bodyRow.leadingAnchor),
|
|
|
+ mainContentColumn.topAnchor.constraint(equalTo: bodyRow.topAnchor),
|
|
|
+ mainContentColumn.bottomAnchor.constraint(equalTo: bodyRow.bottomAnchor),
|
|
|
+ mainContentColumn.trailingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor, constant: -homeEmbeddedBrowserColumnGap),
|
|
|
+
|
|
|
+ rightWebPanel.topAnchor.constraint(equalTo: bodyRow.topAnchor),
|
|
|
+ rightWebPanel.bottomAnchor.constraint(equalTo: bodyRow.bottomAnchor),
|
|
|
+ rightWebPanel.trailingAnchor.constraint(equalTo: bodyRow.trailingAnchor),
|
|
|
+ rightPanelWidthConstraint,
|
|
|
+
|
|
|
+ webToolbar.leadingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor),
|
|
|
+ webToolbar.trailingAnchor.constraint(equalTo: rightWebPanel.trailingAnchor),
|
|
|
+ webToolbar.topAnchor.constraint(equalTo: rightWebPanel.topAnchor),
|
|
|
+ webToolbar.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+
|
|
|
+ webToolBarStack.leadingAnchor.constraint(equalTo: webToolbar.leadingAnchor, constant: 8),
|
|
|
+ webToolBarStack.trailingAnchor.constraint(equalTo: webToolbar.trailingAnchor, constant: -8),
|
|
|
+ webToolBarStack.centerYAnchor.constraint(equalTo: webToolbar.centerYAnchor),
|
|
|
+
|
|
|
+ embeddedWK.topAnchor.constraint(equalTo: webToolbar.bottomAnchor),
|
|
|
+ embeddedWK.leadingAnchor.constraint(equalTo: rightWebPanel.leadingAnchor),
|
|
|
+ embeddedWK.trailingAnchor.constraint(equalTo: rightWebPanel.trailingAnchor),
|
|
|
+ embeddedWK.bottomAnchor.constraint(equalTo: rightWebPanel.bottomAnchor),
|
|
|
|
|
|
searchRowCenterX,
|
|
|
searchRow.centerYAnchor.constraint(equalTo: topBar.centerYAnchor),
|
|
|
@@ -5141,24 +5409,24 @@ class ViewController: NSViewController {
|
|
|
searchField.centerYAnchor.constraint(equalTo: searchPill.centerYAnchor),
|
|
|
authButton.heightAnchor.constraint(equalToConstant: 30),
|
|
|
|
|
|
- timeTitle.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 20),
|
|
|
- timeTitle.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
|
|
|
+ timeTitle.topAnchor.constraint(equalTo: mainContentColumn.topAnchor, constant: 20),
|
|
|
+ timeTitle.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
|
|
|
dateTitle.topAnchor.constraint(equalTo: timeTitle.bottomAnchor, constant: 6),
|
|
|
- dateTitle.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
|
|
|
+ dateTitle.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
|
|
|
|
|
|
actions.topAnchor.constraint(equalTo: dateTitle.bottomAnchor, constant: 28),
|
|
|
- actions.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
|
|
|
- actions.leadingAnchor.constraint(greaterThanOrEqualTo: contentColumn.leadingAnchor, constant: 12),
|
|
|
- actions.trailingAnchor.constraint(lessThanOrEqualTo: contentColumn.trailingAnchor, constant: -12),
|
|
|
+ actions.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
|
|
|
+ actions.leadingAnchor.constraint(greaterThanOrEqualTo: mainContentColumn.leadingAnchor, constant: 12),
|
|
|
+ actions.trailingAnchor.constraint(lessThanOrEqualTo: mainContentColumn.trailingAnchor, constant: -12),
|
|
|
actions.heightAnchor.constraint(equalToConstant: 100),
|
|
|
|
|
|
panel.topAnchor.constraint(equalTo: actions.bottomAnchor, constant: 18),
|
|
|
- panel.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
|
|
|
+ panel.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
|
|
|
panel.widthAnchor.constraint(equalToConstant: 610),
|
|
|
- panel.leadingAnchor.constraint(greaterThanOrEqualTo: contentColumn.leadingAnchor, constant: 6),
|
|
|
- panel.trailingAnchor.constraint(lessThanOrEqualTo: contentColumn.trailingAnchor, constant: -6),
|
|
|
+ panel.leadingAnchor.constraint(greaterThanOrEqualTo: mainContentColumn.leadingAnchor, constant: 6),
|
|
|
+ panel.trailingAnchor.constraint(lessThanOrEqualTo: mainContentColumn.trailingAnchor, constant: -6),
|
|
|
panel.heightAnchor.constraint(greaterThanOrEqualToConstant: 280),
|
|
|
- panel.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -14),
|
|
|
+ panel.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor, constant: -14),
|
|
|
|
|
|
panelHeader.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
|
|
|
panelHeader.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 16),
|
|
|
@@ -5186,23 +5454,23 @@ class ViewController: NSViewController {
|
|
|
refreshMeetingsButton.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12),
|
|
|
refreshMeetingsButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
|
- placeholder.centerXAnchor.constraint(equalTo: contentColumn.centerXAnchor),
|
|
|
- placeholder.centerYAnchor.constraint(equalTo: contentColumn.centerYAnchor),
|
|
|
+ placeholder.centerXAnchor.constraint(equalTo: mainContentColumn.centerXAnchor),
|
|
|
+ placeholder.centerYAnchor.constraint(equalTo: mainContentColumn.centerYAnchor),
|
|
|
|
|
|
- settingsView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
|
|
|
- settingsView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
|
|
|
- settingsView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
|
|
|
- settingsView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
|
|
|
+ settingsView.topAnchor.constraint(equalTo: mainContentColumn.topAnchor),
|
|
|
+ settingsView.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor),
|
|
|
+ settingsView.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor),
|
|
|
+ settingsView.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor),
|
|
|
|
|
|
- meetingsPageView.topAnchor.constraint(equalTo: contentColumn.topAnchor),
|
|
|
- meetingsPageView.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor),
|
|
|
- meetingsPageView.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor),
|
|
|
- meetingsPageView.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor),
|
|
|
+ meetingsPageView.topAnchor.constraint(equalTo: mainContentColumn.topAnchor),
|
|
|
+ meetingsPageView.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor),
|
|
|
+ meetingsPageView.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor),
|
|
|
+ meetingsPageView.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor),
|
|
|
|
|
|
- schedulerRoot.topAnchor.constraint(equalTo: contentColumn.topAnchor, constant: 8),
|
|
|
- schedulerRoot.leadingAnchor.constraint(equalTo: contentColumn.leadingAnchor, constant: 8),
|
|
|
- schedulerRoot.trailingAnchor.constraint(equalTo: contentColumn.trailingAnchor, constant: -8),
|
|
|
- schedulerRoot.bottomAnchor.constraint(equalTo: contentColumn.bottomAnchor, constant: -8),
|
|
|
+ schedulerRoot.topAnchor.constraint(equalTo: mainContentColumn.topAnchor, constant: 8),
|
|
|
+ schedulerRoot.leadingAnchor.constraint(equalTo: mainContentColumn.leadingAnchor, constant: 8),
|
|
|
+ schedulerRoot.trailingAnchor.constraint(equalTo: mainContentColumn.trailingAnchor, constant: -8),
|
|
|
+ schedulerRoot.bottomAnchor.constraint(equalTo: mainContentColumn.bottomAnchor, constant: -8),
|
|
|
|
|
|
schedulerSplit.topAnchor.constraint(equalTo: schedulerRoot.topAnchor),
|
|
|
schedulerSplit.leadingAnchor.constraint(equalTo: schedulerRoot.leadingAnchor),
|
|
|
@@ -5353,6 +5621,16 @@ class ViewController: NSViewController {
|
|
|
applyAuthenticationUIState(triggerInitialRefresh: false)
|
|
|
updatePremiumLockedSections()
|
|
|
|
|
|
+ embeddedBrowserWebView = embeddedWK
|
|
|
+ embeddedBrowserPanel = rightWebPanel
|
|
|
+ homeBodyRowView = bodyRow
|
|
|
+ homeMainContentColumnView = mainContentColumn
|
|
|
+ embeddedBrowserPanelWidthConstraint = rightPanelWidthConstraint
|
|
|
+ embeddedBrowserMainColumnCollapsedWidthConstraint = mainColumnCollapsedWidthConstraint
|
|
|
+ embeddedBrowserBackButton = browserBack
|
|
|
+ embeddedBrowserForwardButton = browserForward
|
|
|
+ refreshEmbeddedBrowserNavButtons()
|
|
|
+
|
|
|
return root
|
|
|
}
|
|
|
|
|
|
@@ -5889,6 +6167,7 @@ class ViewController: NSViewController {
|
|
|
|
|
|
@objc private func homeSidebarItemTapped(_ sender: NSButton) {
|
|
|
guard let item = sender.identifier?.rawValue else { return }
|
|
|
+ closeEmbeddedBrowserPanel()
|
|
|
selectedHomeSidebarItem = item
|
|
|
updateHomeSidebarHighlight()
|
|
|
updateSelectedHomeSectionUI()
|
|
|
@@ -5903,6 +6182,7 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
|
|
|
func menuBarShowSection(_ section: HomeSection) {
|
|
|
+ closeEmbeddedBrowserPanel()
|
|
|
selectedHomeSidebarItem = section.rawValue
|
|
|
updateHomeSidebarHighlight()
|
|
|
updateSelectedHomeSectionUI()
|
|
|
@@ -5913,10 +6193,11 @@ class ViewController: NSViewController {
|
|
|
showSimpleAlert(title: "Sign in required", message: "Please sign in with Zoom to start a new meeting.")
|
|
|
return
|
|
|
}
|
|
|
- guard let url = URL(string: "https://zoom.us/start/videomeeting"), NSWorkspace.shared.open(url) else {
|
|
|
+ guard let url = URL(string: "https://zoom.us/start/videomeeting") else {
|
|
|
showSimpleAlert(title: "Unable to open", message: "Could not start a new Zoom meeting right now.")
|
|
|
return
|
|
|
}
|
|
|
+ openWebURLPreferringInApp(url)
|
|
|
}
|
|
|
|
|
|
func menuBarTriggerJoinMeeting() {
|
|
|
@@ -6215,7 +6496,14 @@ class ViewController: NSViewController {
|
|
|
let hoverBorder = palette.isDarkMode
|
|
|
? NSColor.white.withAlphaComponent(0.85)
|
|
|
: NSColor.black.withAlphaComponent(0.22)
|
|
|
- let card = MeetingCardView(url: meeting.webURL, hoverBackground: hoverBackground, hoverBorder: hoverBorder)
|
|
|
+ let card = MeetingCardView(
|
|
|
+ url: meeting.webURL,
|
|
|
+ hoverBackground: hoverBackground,
|
|
|
+ hoverBorder: hoverBorder,
|
|
|
+ onTapURL: { [weak self] url in
|
|
|
+ self?.openWebURLPreferringInApp(url)
|
|
|
+ }
|
|
|
+ )
|
|
|
card.wantsLayer = true
|
|
|
card.layer?.backgroundColor = meetingCardBackground.cgColor
|
|
|
card.layer?.cornerRadius = 13
|
|
|
@@ -6300,6 +6588,45 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+extension ViewController: EmbeddedOAuthPresenting {}
|
|
|
+
|
|
|
+extension ViewController: WKNavigationDelegate, WKUIDelegate {
|
|
|
+ func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
|
+ guard webView === embeddedBrowserWebView else {
|
|
|
+ decisionHandler(.allow)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if let prefix = oauthSessionCallbackPrefix,
|
|
|
+ let targetURL = navigationAction.request.url,
|
|
|
+ targetURL.absoluteString.hasPrefix(prefix) {
|
|
|
+ oauthSessionContinuation?.resume(returning: targetURL)
|
|
|
+ oauthSessionContinuation = nil
|
|
|
+ oauthSessionCallbackPrefix = nil
|
|
|
+ decisionHandler(.cancel)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ decisionHandler(.allow)
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
|
+ guard webView === embeddedBrowserWebView else { return }
|
|
|
+ refreshEmbeddedBrowserNavButtons()
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
+ guard webView === embeddedBrowserWebView else { return }
|
|
|
+ refreshEmbeddedBrowserNavButtons()
|
|
|
+ }
|
|
|
+
|
|
|
+ func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
|
+ guard webView === embeddedBrowserWebView else { return nil }
|
|
|
+ if navigationAction.targetFrame == nil {
|
|
|
+ webView.load(navigationAction.request)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
extension ViewController: NSWindowDelegate {
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
|
guard let window = notification.object as? NSWindow else { return }
|
|
|
@@ -6505,16 +6832,18 @@ private final class MeetingCardView: NSView {
|
|
|
private let url: URL?
|
|
|
private let hoverBackground: NSColor
|
|
|
private let hoverBorder: NSColor
|
|
|
+ private let onTapURL: ((URL) -> Void)?
|
|
|
private var tracking: NSTrackingArea?
|
|
|
private var isHovering = false
|
|
|
private var normalBackgroundColor: CGColor?
|
|
|
private var normalBorderColor: CGColor?
|
|
|
private var normalBorderWidth: CGFloat?
|
|
|
|
|
|
- init(url: URL?, hoverBackground: NSColor, hoverBorder: NSColor) {
|
|
|
+ init(url: URL?, hoverBackground: NSColor, hoverBorder: NSColor, onTapURL: ((URL) -> Void)? = nil) {
|
|
|
self.url = url
|
|
|
self.hoverBackground = hoverBackground
|
|
|
self.hoverBorder = hoverBorder
|
|
|
+ self.onTapURL = onTapURL
|
|
|
super.init(frame: .zero)
|
|
|
}
|
|
|
|
|
|
@@ -6522,6 +6851,7 @@ private final class MeetingCardView: NSView {
|
|
|
self.url = nil
|
|
|
self.hoverBackground = NSColor.white.withAlphaComponent(0.075)
|
|
|
self.hoverBorder = NSColor.white.withAlphaComponent(0.85)
|
|
|
+ self.onTapURL = nil
|
|
|
super.init(coder: coder)
|
|
|
}
|
|
|
|
|
|
@@ -6558,7 +6888,11 @@ private final class MeetingCardView: NSView {
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
|
super.mouseUp(with: event)
|
|
|
guard let url else { return }
|
|
|
- NSWorkspace.shared.open(url)
|
|
|
+ if let onTapURL {
|
|
|
+ onTapURL(url)
|
|
|
+ } else {
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func applyHoverState() {
|
|
|
@@ -7038,6 +7372,8 @@ final class KeychainTokenStore {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// MARK: - OAuth window (previous sign-in behavior)
|
|
|
+
|
|
|
@MainActor
|
|
|
private final class OAuthWebViewContainerView: NSView {
|
|
|
private let webView: WKWebView
|