|
@@ -38,6 +38,24 @@ protocol EmbeddedOAuthPresenting: AnyObject {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
class ViewController: NSViewController {
|
|
class ViewController: NSViewController {
|
|
|
|
|
+ private static let embeddedZoomMeetingLifecycleHandlerName = "zoomAppMeetingLifecycle"
|
|
|
|
|
+ private static let embeddedZoomMeetingLifecycleJavaScript = """
|
|
|
|
|
+ (function() {
|
|
|
|
|
+ function notify() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ window.webkit.messageHandlers.\(embeddedZoomMeetingLifecycleHandlerName).postMessage(window.location.href || '');
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+ var pushState = history.pushState;
|
|
|
|
|
+ history.pushState = function() { pushState.apply(this, arguments); notify(); };
|
|
|
|
|
+ var replaceState = history.replaceState;
|
|
|
|
|
+ history.replaceState = function() { replaceState.apply(this, arguments); notify(); };
|
|
|
|
|
+ window.addEventListener('popstate', notify);
|
|
|
|
|
+ window.addEventListener('hashchange', notify);
|
|
|
|
|
+ setInterval(notify, 1500);
|
|
|
|
|
+ })();
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
enum HomeSection: String {
|
|
enum HomeSection: String {
|
|
|
case home = "Home"
|
|
case home = "Home"
|
|
|
case meetings = "Meetings"
|
|
case meetings = "Meetings"
|
|
@@ -452,6 +470,9 @@ class ViewController: NSViewController {
|
|
|
private weak var meetingConsentSecondaryLanguagePopup: NSPopUpButton?
|
|
private weak var meetingConsentSecondaryLanguagePopup: NSPopUpButton?
|
|
|
|
|
|
|
|
private var aiCompanionTranscriptionActiveRecordingIDs: Set<String> = []
|
|
private var aiCompanionTranscriptionActiveRecordingIDs: Set<String> = []
|
|
|
|
|
+ /// True after the embedded web client loaded a join/in-call URL during the current recording session.
|
|
|
|
|
+ private var embeddedMeetingReachedInCallPage = false
|
|
|
|
|
+ private var embeddedMeetingRecordingMonitorTimer: Timer?
|
|
|
|
|
|
|
|
private enum LocalRecordingNotesStatus: String {
|
|
private enum LocalRecordingNotesStatus: String {
|
|
|
case notRequested
|
|
case notRequested
|
|
@@ -753,6 +774,9 @@ class ViewController: NSViewController {
|
|
|
) { [weak self] _ in
|
|
) { [weak self] _ in
|
|
|
Task { @MainActor [weak self] in
|
|
Task { @MainActor [weak self] in
|
|
|
guard let self else { return }
|
|
guard let self else { return }
|
|
|
|
|
+ if MeetingRecordingManager.shared.isRecording == false {
|
|
|
|
|
+ self.stopEmbeddedMeetingRecordingMonitor()
|
|
|
|
|
+ }
|
|
|
self.updateAiCompanionRecordingUI()
|
|
self.updateAiCompanionRecordingUI()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -2083,6 +2107,116 @@ class ViewController: NSViewController {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /// Active join / in-call pages in the embedded Zoom web client (not Workplace hub routes under `/wc/`).
|
|
|
|
|
+ private func isZoomEmbeddedWebClientInCallURL(_ url: URL) -> Bool {
|
|
|
|
|
+ guard let host = url.host?.lowercased(), host.contains("zoom.") else { return false }
|
|
|
|
|
+ if isZoomMeetingLeaveOrEndURL(url) { return false }
|
|
|
|
|
+ let path = url.path.lowercased()
|
|
|
|
|
+ if path.contains("/wc/join") { return true }
|
|
|
|
|
+ if path.contains("/wc/inmeeting") { return true }
|
|
|
|
|
+ if path.contains("/wc/video") { return true }
|
|
|
|
|
+ // Non–web-client join links opened in the embedded panel (rare).
|
|
|
|
|
+ if path.contains("/wc/") == false {
|
|
|
|
|
+ return isZoomMeetingJoinOrStartURL(url)
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Workplace hub, sign-in, or explicit post-meeting pages — not an active call.
|
|
|
|
|
+ private func isZoomMeetingLeaveOrEndURL(_ url: URL) -> Bool {
|
|
|
|
|
+ guard let host = url.host?.lowercased(), host.contains("zoom.") else { return false }
|
|
|
|
|
+ let path = url.path.lowercased()
|
|
|
|
|
+ let absolute = url.absoluteString.lowercased()
|
|
|
|
|
+ if path.contains("/leave") || path.contains("postattendee") || path.contains("/feedback") { return true }
|
|
|
|
|
+ if absolute.contains("meeting_ended") || absolute.contains("meeting-ended") { return true }
|
|
|
|
|
+
|
|
|
|
|
+ let workplaceHubMarkers = [
|
|
|
|
|
+ "/wc/home", "/wc/profile", "/wc/calendar", "/wc/chat", "/wc/mail",
|
|
|
|
|
+ "/wc/whiteboard", "/wc/meeting", "/wc/recordings", "/wc/notes",
|
|
|
|
|
+ "/wc/contacts", "/wc/scheduler", "/wc/documents", "/wc/apps"
|
|
|
|
|
+ ]
|
|
|
|
|
+ if workplaceHubMarkers.contains(where: { path == $0 || path.hasPrefix($0 + "/") }) { return true }
|
|
|
|
|
+ if path == "/wc" || path == "/wc/" { return true }
|
|
|
|
|
+
|
|
|
|
|
+ if path.contains("/wc/") == false, path.isEmpty || path == "/" || path.hasPrefix("/signin") { return true }
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func embeddedWebViewTitleIndicatesLeftMeeting(_ title: String) -> Bool {
|
|
|
|
|
+ let t = title.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
|
|
|
+ guard t.isEmpty == false else { return false }
|
|
|
|
|
+ if t.contains("zoom meeting") || t.contains("zoom webinar") { return false }
|
|
|
|
|
+ if t.contains("waiting for the host") || t.contains("waiting for host") { return false }
|
|
|
|
|
+ let hubTitles = [
|
|
|
|
|
+ "zoom workplace", "meetings | zoom", "home | zoom", "calendar | zoom",
|
|
|
|
|
+ "team chat | zoom", "chat | zoom", "sign in", "sign-in", "thank you for"
|
|
|
|
|
+ ]
|
|
|
|
|
+ return hubTitles.contains(where: { t.contains($0) })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @MainActor
|
|
|
|
|
+ private func updateEmbeddedMeetingRecordingPresence(for url: URL?, webViewTitle: String? = nil) {
|
|
|
|
|
+ guard MeetingRecordingManager.shared.isRecording else {
|
|
|
|
|
+ stopEmbeddedMeetingRecordingMonitor()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let inCallURL = url.map { isZoomEmbeddedWebClientInCallURL($0) } ?? false
|
|
|
|
|
+ let leftByURL = url.map { isZoomMeetingLeaveOrEndURL($0) } ?? false
|
|
|
|
|
+ let leftByTitle = webViewTitle.map { embeddedWebViewTitleIndicatesLeftMeeting($0) } ?? false
|
|
|
|
|
+ let inCall = inCallURL && !leftByURL
|
|
|
|
|
+
|
|
|
|
|
+ if inCall {
|
|
|
|
|
+ embeddedMeetingReachedInCallPage = true
|
|
|
|
|
+ }
|
|
|
|
|
+ MeetingRecordingManager.shared.setEmbeddedWebClientCallUIVisible(inCall)
|
|
|
|
|
+
|
|
|
|
|
+ let shouldStopNow = leftByURL || leftByTitle
|
|
|
|
|
+ || (embeddedMeetingReachedInCallPage && !inCall)
|
|
|
|
|
+ if shouldStopNow {
|
|
|
|
|
+ endZoomMeetingRecordingAutomatically()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @MainActor
|
|
|
|
|
+ private func startEmbeddedMeetingRecordingMonitorIfNeeded() {
|
|
|
|
|
+ stopEmbeddedMeetingRecordingMonitor()
|
|
|
|
|
+ guard MeetingRecordingManager.shared.isRecording else { return }
|
|
|
|
|
+ guard embeddedBrowserWebView != nil else { return }
|
|
|
|
|
+ embeddedMeetingReachedInCallPage = false
|
|
|
|
|
+ let timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
|
|
|
+ guard let self else { return }
|
|
|
|
|
+ let webView = self.embeddedBrowserWebView
|
|
|
|
|
+ self.updateEmbeddedMeetingRecordingPresence(for: webView?.url, webViewTitle: webView?.title)
|
|
|
|
|
+ }
|
|
|
|
|
+ RunLoop.main.add(timer, forMode: .common)
|
|
|
|
|
+ embeddedMeetingRecordingMonitorTimer = timer
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @MainActor
|
|
|
|
|
+ private func stopEmbeddedMeetingRecordingMonitor() {
|
|
|
|
|
+ embeddedMeetingRecordingMonitorTimer?.invalidate()
|
|
|
|
|
+ embeddedMeetingRecordingMonitorTimer = nil
|
|
|
|
|
+ embeddedMeetingReachedInCallPage = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @MainActor
|
|
|
|
|
+ private func endZoomMeetingRecordingAutomatically() {
|
|
|
|
|
+ guard MeetingRecordingManager.shared.isRecording else { return }
|
|
|
|
|
+ stopEmbeddedMeetingRecordingMonitor()
|
|
|
|
|
+ Task { @MainActor in
|
|
|
|
|
+ guard MeetingRecordingManager.shared.isRecording else { return }
|
|
|
|
|
+ do {
|
|
|
|
|
+ try await MeetingRecordingManager.shared.stopRecording()
|
|
|
|
|
+ self.recordedMeetings = MeetingRecordingManager.shared.recordings
|
|
|
|
|
+ self.renderAiCompanionList()
|
|
|
|
|
+ self.updateAiCompanionRecordingUI()
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ NSLog("ViewController: auto-stop recording failed — \(error.localizedDescription)")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@MainActor
|
|
@MainActor
|
|
|
private func beginZoomMeetingRecordingIfAppropriate(sourceURL: URL, meetingDisplayName: String?) {
|
|
private func beginZoomMeetingRecordingIfAppropriate(sourceURL: URL, meetingDisplayName: String?) {
|
|
|
guard isZoomMeetingJoinOrStartURL(sourceURL) else { return }
|
|
guard isZoomMeetingJoinOrStartURL(sourceURL) else { return }
|
|
@@ -2091,6 +2225,10 @@ class ViewController: NSViewController {
|
|
|
do {
|
|
do {
|
|
|
try await MeetingRecordingManager.shared.startRecording(sourceURL: sourceURL, meetingDisplayName: meetingDisplayName)
|
|
try await MeetingRecordingManager.shared.startRecording(sourceURL: sourceURL, meetingDisplayName: meetingDisplayName)
|
|
|
self.updateAiCompanionRecordingUI()
|
|
self.updateAiCompanionRecordingUI()
|
|
|
|
|
+ if self.embeddedBrowserWebView != nil,
|
|
|
|
|
+ (self.embeddedBrowserPanelWidthConstraint?.constant ?? 0) > 0.5 {
|
|
|
|
|
+ self.startEmbeddedMeetingRecordingMonitorIfNeeded()
|
|
|
|
|
+ }
|
|
|
} catch {
|
|
} catch {
|
|
|
self.showSimpleAlert(title: "Recording could not start", message: error.localizedDescription)
|
|
self.showSimpleAlert(title: "Recording could not start", message: error.localizedDescription)
|
|
|
}
|
|
}
|
|
@@ -2113,6 +2251,7 @@ class ViewController: NSViewController {
|
|
|
Task { await ensureMediaCaptureAccessForEmbeddedZoomWebClient() }
|
|
Task { await ensureMediaCaptureAccessForEmbeddedZoomWebClient() }
|
|
|
}
|
|
}
|
|
|
webView.load(URLRequest(url: url))
|
|
webView.load(URLRequest(url: url))
|
|
|
|
|
+ updateEmbeddedMeetingRecordingPresence(for: url)
|
|
|
refreshEmbeddedBrowserNavButtons()
|
|
refreshEmbeddedBrowserNavButtons()
|
|
|
view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
view.window?.contentView?.layoutSubtreeIfNeeded()
|
|
|
}
|
|
}
|
|
@@ -2193,12 +2332,17 @@ class ViewController: NSViewController {
|
|
|
|
|
|
|
|
@MainActor
|
|
@MainActor
|
|
|
private func closeEmbeddedBrowserPanel() {
|
|
private func closeEmbeddedBrowserPanel() {
|
|
|
|
|
+ if MeetingRecordingManager.shared.isRecording {
|
|
|
|
|
+ endZoomMeetingRecordingAutomatically()
|
|
|
|
|
+ }
|
|
|
if let cont = oauthSessionContinuation {
|
|
if let cont = oauthSessionContinuation {
|
|
|
oauthSessionContinuation = nil
|
|
oauthSessionContinuation = nil
|
|
|
oauthSessionCallbackPrefix = nil
|
|
oauthSessionCallbackPrefix = nil
|
|
|
cont.resume(throwing: EmbeddedOAuthFlowError.userCancelled)
|
|
cont.resume(throwing: EmbeddedOAuthFlowError.userCancelled)
|
|
|
}
|
|
}
|
|
|
embeddedBrowserWebView?.stopLoading()
|
|
embeddedBrowserWebView?.stopLoading()
|
|
|
|
|
+ stopEmbeddedMeetingRecordingMonitor()
|
|
|
|
|
+ MeetingRecordingManager.shared.setEmbeddedWebClientCallUIVisible(false)
|
|
|
embeddedBrowserPanelWidthConstraint?.constant = 0
|
|
embeddedBrowserPanelWidthConstraint?.constant = 0
|
|
|
embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = false
|
|
embeddedBrowserMainColumnCollapsedWidthConstraint?.isActive = false
|
|
|
homeMainContentColumnView?.isHidden = false
|
|
homeMainContentColumnView?.isHidden = false
|
|
@@ -6594,6 +6738,15 @@ class ViewController: NSViewController {
|
|
|
let embeddedBrowserConfiguration = WKWebViewConfiguration()
|
|
let embeddedBrowserConfiguration = WKWebViewConfiguration()
|
|
|
embeddedBrowserConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
embeddedBrowserConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
embeddedBrowserConfiguration.mediaTypesRequiringUserActionForPlayback = []
|
|
embeddedBrowserConfiguration.mediaTypesRequiringUserActionForPlayback = []
|
|
|
|
|
+ let meetingLifecycleController = WKUserContentController()
|
|
|
|
|
+ meetingLifecycleController.add(self, name: Self.embeddedZoomMeetingLifecycleHandlerName)
|
|
|
|
|
+ let lifecycleScript = WKUserScript(
|
|
|
|
|
+ source: Self.embeddedZoomMeetingLifecycleJavaScript,
|
|
|
|
|
+ injectionTime: .atDocumentEnd,
|
|
|
|
|
+ forMainFrameOnly: true
|
|
|
|
|
+ )
|
|
|
|
|
+ meetingLifecycleController.addUserScript(lifecycleScript)
|
|
|
|
|
+ embeddedBrowserConfiguration.userContentController = meetingLifecycleController
|
|
|
let embeddedWK = WKWebView(frame: .zero, configuration: embeddedBrowserConfiguration)
|
|
let embeddedWK = WKWebView(frame: .zero, configuration: embeddedBrowserConfiguration)
|
|
|
embeddedWK.customUserAgent = embeddedZoomWebViewUserAgent
|
|
embeddedWK.customUserAgent = embeddedZoomWebViewUserAgent
|
|
|
embeddedWK.translatesAutoresizingMaskIntoConstraints = false
|
|
embeddedWK.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -7429,7 +7582,7 @@ class ViewController: NSViewController {
|
|
|
aiCompanionStopRecordingButton?.isHidden = !mgr.isRecording
|
|
aiCompanionStopRecordingButton?.isHidden = !mgr.isRecording
|
|
|
if mgr.isRecording {
|
|
if mgr.isRecording {
|
|
|
aiCompanionStatusLabel?.stringValue =
|
|
aiCompanionStatusLabel?.stringValue =
|
|
|
- "Recording… When you leave the meeting, click Stop & save (here or in the menu bar)."
|
|
|
|
|
|
|
+ "Recording… Stops automatically when you leave or the meeting ends."
|
|
|
aiCompanionStatusLabel?.font = .systemFont(ofSize: 12, weight: .regular)
|
|
aiCompanionStatusLabel?.font = .systemFont(ofSize: 12, weight: .regular)
|
|
|
aiCompanionStatusLabel?.textColor = secondaryText
|
|
aiCompanionStatusLabel?.textColor = secondaryText
|
|
|
} else {
|
|
} else {
|
|
@@ -7451,6 +7604,7 @@ class ViewController: NSViewController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func aiCompanionStopRecordingTapped() {
|
|
@objc private func aiCompanionStopRecordingTapped() {
|
|
|
|
|
+ stopEmbeddedMeetingRecordingMonitor()
|
|
|
Task { @MainActor in
|
|
Task { @MainActor in
|
|
|
do {
|
|
do {
|
|
|
try await MeetingRecordingManager.shared.stopRecording()
|
|
try await MeetingRecordingManager.shared.stopRecording()
|
|
@@ -7537,13 +7691,15 @@ class ViewController: NSViewController {
|
|
|
return "Saved time: \(formatter.string(from: date))"
|
|
return "Saved time: \(formatter.string(from: date))"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private let aiCompanionGetNotesButtonHeight: CGFloat = 28
|
|
|
|
|
+
|
|
|
private func styleAiCompanionGetNotesButton(_ button: NSButton, enabled: Bool) {
|
|
private func styleAiCompanionGetNotesButton(_ button: NSButton, enabled: Bool) {
|
|
|
- let pillHeight: CGFloat = 36
|
|
|
|
|
|
|
+ let pillHeight = aiCompanionGetNotesButtonHeight
|
|
|
button.isBordered = false
|
|
button.isBordered = false
|
|
|
button.wantsLayer = true
|
|
button.wantsLayer = true
|
|
|
button.layer?.cornerRadius = pillHeight / 2
|
|
button.layer?.cornerRadius = pillHeight / 2
|
|
|
button.layer?.borderWidth = 1
|
|
button.layer?.borderWidth = 1
|
|
|
- button.font = .systemFont(ofSize: 13, weight: .semibold)
|
|
|
|
|
|
|
+ button.font = .systemFont(ofSize: 12, weight: .semibold)
|
|
|
button.imagePosition = .imageLeading
|
|
button.imagePosition = .imageLeading
|
|
|
button.imageHugsTitle = true
|
|
button.imageHugsTitle = true
|
|
|
|
|
|
|
@@ -7559,7 +7715,7 @@ class ViewController: NSViewController {
|
|
|
button.layer?.borderColor = accentBlue.cgColor
|
|
button.layer?.borderColor = accentBlue.cgColor
|
|
|
button.contentTintColor = accentBlue
|
|
button.contentTintColor = accentBlue
|
|
|
if let icon = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil) {
|
|
if let icon = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil) {
|
|
|
- let config = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
|
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
.applying(NSImage.SymbolConfiguration(hierarchicalColor: accentBlue))
|
|
.applying(NSImage.SymbolConfiguration(hierarchicalColor: accentBlue))
|
|
|
button.image = icon.withSymbolConfiguration(config)
|
|
button.image = icon.withSymbolConfiguration(config)
|
|
|
}
|
|
}
|
|
@@ -7569,7 +7725,7 @@ class ViewController: NSViewController {
|
|
|
button.layer?.borderColor = mutedBlue.cgColor
|
|
button.layer?.borderColor = mutedBlue.cgColor
|
|
|
button.contentTintColor = mutedBlue
|
|
button.contentTintColor = mutedBlue
|
|
|
if let icon = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil) {
|
|
if let icon = NSImage(systemSymbolName: "note.text", accessibilityDescription: nil) {
|
|
|
- let config = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
|
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
.applying(NSImage.SymbolConfiguration(hierarchicalColor: mutedBlue))
|
|
.applying(NSImage.SymbolConfiguration(hierarchicalColor: mutedBlue))
|
|
|
button.image = icon.withSymbolConfiguration(config)
|
|
button.image = icon.withSymbolConfiguration(config)
|
|
|
}
|
|
}
|
|
@@ -7671,10 +7827,10 @@ class ViewController: NSViewController {
|
|
|
savedTimeLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
savedTimeLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
savedTimeLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
savedTimeLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
|
|
|
|
|
|
- getNotesBtn.topAnchor.constraint(equalTo: savedTimeLabel.bottomAnchor, constant: 14),
|
|
|
|
|
|
|
+ getNotesBtn.topAnchor.constraint(equalTo: savedTimeLabel.bottomAnchor, constant: 12),
|
|
|
getNotesBtn.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
getNotesBtn.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
- getNotesBtn.heightAnchor.constraint(equalToConstant: 36),
|
|
|
|
|
- getNotesBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 140),
|
|
|
|
|
|
|
+ getNotesBtn.heightAnchor.constraint(equalToConstant: aiCompanionGetNotesButtonHeight),
|
|
|
|
|
+ getNotesBtn.widthAnchor.constraint(greaterThanOrEqualToConstant: 108),
|
|
|
|
|
|
|
|
statusLabel.topAnchor.constraint(equalTo: getNotesBtn.bottomAnchor, constant: 8),
|
|
statusLabel.topAnchor.constraint(equalTo: getNotesBtn.bottomAnchor, constant: 8),
|
|
|
statusLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
statusLabel.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
@@ -9532,6 +9688,17 @@ class ViewController: NSViewController {
|
|
|
|
|
|
|
|
extension ViewController: EmbeddedOAuthPresenting {}
|
|
extension ViewController: EmbeddedOAuthPresenting {}
|
|
|
|
|
|
|
|
|
|
+extension ViewController: WKScriptMessageHandler {
|
|
|
|
|
+ func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
|
|
|
+ guard message.name == Self.embeddedZoomMeetingLifecycleHandlerName else { return }
|
|
|
|
|
+ guard let href = message.body as? String, let url = URL(string: href) else { return }
|
|
|
|
|
+ Task { @MainActor in
|
|
|
|
|
+ let title = self.embeddedBrowserWebView?.title
|
|
|
|
|
+ self.updateEmbeddedMeetingRecordingPresence(for: url, webViewTitle: title)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
extension ViewController: WKNavigationDelegate, WKUIDelegate {
|
|
extension ViewController: WKNavigationDelegate, WKUIDelegate {
|
|
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
|
guard webView === embeddedBrowserWebView else {
|
|
guard webView === embeddedBrowserWebView else {
|
|
@@ -9547,16 +9714,23 @@ extension ViewController: WKNavigationDelegate, WKUIDelegate {
|
|
|
decisionHandler(.cancel)
|
|
decisionHandler(.cancel)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+ if let targetURL = navigationAction.request.url {
|
|
|
|
|
+ Task { @MainActor in
|
|
|
|
|
+ self.updateEmbeddedMeetingRecordingPresence(for: targetURL, webViewTitle: webView.title)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
decisionHandler(.allow)
|
|
decisionHandler(.allow)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
|
guard webView === embeddedBrowserWebView else { return }
|
|
guard webView === embeddedBrowserWebView else { return }
|
|
|
|
|
+ updateEmbeddedMeetingRecordingPresence(for: webView.url, webViewTitle: webView.title)
|
|
|
refreshEmbeddedBrowserNavButtons()
|
|
refreshEmbeddedBrowserNavButtons()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
guard webView === embeddedBrowserWebView else { return }
|
|
guard webView === embeddedBrowserWebView else { return }
|
|
|
|
|
+ updateEmbeddedMeetingRecordingPresence(for: webView.url, webViewTitle: webView.title)
|
|
|
refreshEmbeddedBrowserNavButtons()
|
|
refreshEmbeddedBrowserNavButtons()
|
|
|
}
|
|
}
|
|
|
|
|
|