|
@@ -509,6 +509,8 @@ final class ViewController: NSViewController {
|
|
|
private let aiCompanionLocalRecordingsDefaultsKey = "aiCompanion.localRecordings"
|
|
private let aiCompanionLocalRecordingsDefaultsKey = "aiCompanion.localRecordings"
|
|
|
private let aiCompanionPreferredLanguage1DefaultsKey = "aiCompanion.preferredLanguage1"
|
|
private let aiCompanionPreferredLanguage1DefaultsKey = "aiCompanion.preferredLanguage1"
|
|
|
private let aiCompanionPreferredLanguage2DefaultsKey = "aiCompanion.preferredLanguage2"
|
|
private let aiCompanionPreferredLanguage2DefaultsKey = "aiCompanion.preferredLanguage2"
|
|
|
|
|
+ private weak var activeMeetingConsentWindow: NSWindow?
|
|
|
|
|
+ private var didTapInlineMeetingConsentClose = false
|
|
|
private let ratingEligibleUsageSeconds: TimeInterval = 30 * 60
|
|
private let ratingEligibleUsageSeconds: TimeInterval = 30 * 60
|
|
|
private let meetingTranscriptionService = MeetingTranscriptionService()
|
|
private let meetingTranscriptionService = MeetingTranscriptionService()
|
|
|
private let meetingNotesService = MeetingNotesService()
|
|
private let meetingNotesService = MeetingNotesService()
|
|
@@ -876,7 +878,8 @@ private extension ViewController {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- beginMeetingRecordingIfConsented(meetingTitle: "Quick Join Meeting", meetingURL: url)
|
|
|
|
|
|
|
+ let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: "Quick Join Meeting", meetingURL: url)
|
|
|
|
|
+ guard shouldOpenMeeting else { return }
|
|
|
openInDefaultBrowser(url: url)
|
|
openInDefaultBrowser(url: url)
|
|
|
consumeNonPremiumJoinTrialIfNeeded()
|
|
consumeNonPremiumJoinTrialIfNeeded()
|
|
|
}
|
|
}
|
|
@@ -1319,15 +1322,147 @@ private extension ViewController {
|
|
|
alert.runModal()
|
|
alert.runModal()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func requestMeetingRecordingConsent(title: String) -> Bool {
|
|
|
|
|
|
|
+ private enum MeetingRecordingConsentChoice {
|
|
|
|
|
+ case allowAndContinue
|
|
|
|
|
+ case continueWithoutRecording
|
|
|
|
|
+ case dismissed
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func requestMeetingRecordingConsent(title: String) -> MeetingRecordingConsentChoice {
|
|
|
let alert = NSAlert()
|
|
let alert = NSAlert()
|
|
|
alert.messageText = "Record this meeting locally?"
|
|
alert.messageText = "Record this meeting locally?"
|
|
|
alert.informativeText = "With your consent, the app will record meeting audio on this Mac and show it in AI Companion after the meeting."
|
|
alert.informativeText = "With your consent, the app will record meeting audio on this Mac and show it in AI Companion after the meeting."
|
|
|
alert.addButton(withTitle: "Allow and Continue")
|
|
alert.addButton(withTitle: "Allow and Continue")
|
|
|
alert.addButton(withTitle: "Continue Without Recording")
|
|
alert.addButton(withTitle: "Continue Without Recording")
|
|
|
|
|
+ alert.window.title = title
|
|
|
|
|
+
|
|
|
|
|
+ let speechOptions = aiCompanionSupportedSpeechLocaleOptions()
|
|
|
|
|
+ let languageContainer = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 184))
|
|
|
|
|
+ languageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ languageContainer.widthAnchor.constraint(equalToConstant: 360).isActive = true
|
|
|
|
|
+ languageContainer.heightAnchor.constraint(equalToConstant: 184).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ let languageTitle = NSTextField(labelWithString: "Transcription Languages")
|
|
|
|
|
+ languageTitle.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ languageTitle.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
|
|
+ languageTitle.textColor = NSColor.secondaryLabelColor
|
|
|
|
|
+
|
|
|
|
|
+ let language1Popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
|
|
+ language1Popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ language1Popup.controlSize = .regular
|
|
|
|
|
+ for option in speechOptions {
|
|
|
|
|
+ language1Popup.addItem(withTitle: option.displayName)
|
|
|
|
|
+ language1Popup.lastItem?.representedObject = option.identifier
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let selectedPrimary = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage1DefaultsKey)
|
|
|
|
|
+ ?? speechOptions.first?.identifier
|
|
|
|
|
+ ?? Locale.current.identifier
|
|
|
|
|
+ selectSettingsPageLanguage(identifier: selectedPrimary, in: language1Popup)
|
|
|
|
|
+
|
|
|
|
|
+ let language2Popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
|
|
+ language2Popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ language2Popup.controlSize = .regular
|
|
|
|
|
+ language2Popup.addItem(withTitle: "None")
|
|
|
|
|
+ language2Popup.lastItem?.representedObject = ""
|
|
|
|
|
+ for option in speechOptions {
|
|
|
|
|
+ language2Popup.addItem(withTitle: option.displayName)
|
|
|
|
|
+ language2Popup.lastItem?.representedObject = option.identifier
|
|
|
|
|
+ }
|
|
|
|
|
+ if let selectedSecondary = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage2DefaultsKey),
|
|
|
|
|
+ selectedSecondary.isEmpty == false {
|
|
|
|
|
+ selectSettingsPageLanguage(identifier: selectedSecondary, in: language2Popup)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ language2Popup.selectItem(at: 0)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let language1Label = NSTextField(labelWithString: "Preferred Language 1")
|
|
|
|
|
+ language1Label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ language1Label.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
|
|
+ language1Label.textColor = NSColor.secondaryLabelColor
|
|
|
|
|
+ language1Label.alignment = .center
|
|
|
|
|
+
|
|
|
|
|
+ let language2Label = NSTextField(labelWithString: "Preferred Language 2")
|
|
|
|
|
+ language2Label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ language2Label.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
|
|
+ language2Label.textColor = NSColor.secondaryLabelColor
|
|
|
|
|
+ language2Label.alignment = .center
|
|
|
|
|
+
|
|
|
|
|
+ languageContainer.addSubview(languageTitle)
|
|
|
|
|
+ languageContainer.addSubview(language1Label)
|
|
|
|
|
+ languageContainer.addSubview(language1Popup)
|
|
|
|
|
+ languageContainer.addSubview(language2Label)
|
|
|
|
|
+ languageContainer.addSubview(language2Popup)
|
|
|
|
|
+
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ languageTitle.topAnchor.constraint(equalTo: languageContainer.topAnchor, constant: 4),
|
|
|
|
|
+ languageTitle.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ language1Label.topAnchor.constraint(equalTo: languageTitle.bottomAnchor, constant: 12),
|
|
|
|
|
+ language1Label.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
|
|
+ language1Popup.topAnchor.constraint(equalTo: language1Label.bottomAnchor, constant: 4),
|
|
|
|
|
+ language1Popup.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
|
|
+ language1Popup.widthAnchor.constraint(equalToConstant: 280),
|
|
|
|
|
+
|
|
|
|
|
+ language2Label.topAnchor.constraint(equalTo: language1Popup.bottomAnchor, constant: 10),
|
|
|
|
|
+ language2Label.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
|
|
+ language2Popup.topAnchor.constraint(equalTo: language2Label.bottomAnchor, constant: 4),
|
|
|
|
|
+ language2Popup.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
|
|
+ language2Popup.widthAnchor.constraint(equalToConstant: 280),
|
|
|
|
|
+ language2Popup.bottomAnchor.constraint(lessThanOrEqualTo: languageContainer.bottomAnchor, constant: -4)
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ alert.accessoryView = languageContainer
|
|
|
|
|
+ didTapInlineMeetingConsentClose = false
|
|
|
|
|
+ activeMeetingConsentWindow = alert.window
|
|
|
|
|
+ if let contentView = alert.window.contentView {
|
|
|
|
|
+ let topCloseButton = NSButton(title: "✕", target: self, action: #selector(meetingConsentInlineCloseTapped(_:)))
|
|
|
|
|
+ topCloseButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ topCloseButton.isBordered = false
|
|
|
|
|
+ topCloseButton.font = NSFont.systemFont(ofSize: 15, weight: .semibold)
|
|
|
|
|
+ topCloseButton.contentTintColor = NSColor.tertiaryLabelColor
|
|
|
|
|
+ topCloseButton.wantsLayer = true
|
|
|
|
|
+ topCloseButton.layer?.cornerRadius = 20
|
|
|
|
|
+ topCloseButton.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.35).cgColor
|
|
|
|
|
+ contentView.addSubview(topCloseButton)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ topCloseButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
|
|
|
|
+ topCloseButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
|
|
|
|
|
+ topCloseButton.widthAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
+ topCloseButton.heightAnchor.constraint(equalToConstant: 40)
|
|
|
|
|
+ ])
|
|
|
|
|
+ }
|
|
|
let response = alert.runModal()
|
|
let response = alert.runModal()
|
|
|
- if response == .alertFirstButtonReturn { return true }
|
|
|
|
|
- return false
|
|
|
|
|
|
|
+ activeMeetingConsentWindow = nil
|
|
|
|
|
+
|
|
|
|
|
+ if didTapInlineMeetingConsentClose {
|
|
|
|
|
+ return .dismissed
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if let primary = language1Popup.selectedItem?.representedObject as? String,
|
|
|
|
|
+ primary.isEmpty == false {
|
|
|
|
|
+ var secondary = language2Popup.selectedItem?.representedObject as? String
|
|
|
|
|
+ if secondary?.isEmpty == true {
|
|
|
|
|
+ secondary = nil
|
|
|
|
|
+ }
|
|
|
|
|
+ updateAiCompanionPreferredSpeechLanguages(primary: primary, secondary: secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch response {
|
|
|
|
|
+ case .alertFirstButtonReturn:
|
|
|
|
|
+ return .allowAndContinue
|
|
|
|
|
+ case .alertSecondButtonReturn:
|
|
|
|
|
+ return .continueWithoutRecording
|
|
|
|
|
+ default:
|
|
|
|
|
+ return .dismissed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @objc private func meetingConsentInlineCloseTapped(_ sender: NSButton) {
|
|
|
|
|
+ didTapInlineMeetingConsentClose = true
|
|
|
|
|
+ guard let consentWindow = activeMeetingConsentWindow else { return }
|
|
|
|
|
+ NSApp.stopModal(withCode: .abort)
|
|
|
|
|
+ consentWindow.orderOut(nil)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func loadAiCompanionLocalRecordings() {
|
|
private func loadAiCompanionLocalRecordings() {
|
|
@@ -1523,11 +1658,16 @@ private extension ViewController {
|
|
|
return directory
|
|
return directory
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func beginMeetingRecordingIfConsented(meetingTitle: String, meetingURL: URL) {
|
|
|
|
|
|
|
+ private func beginMeetingRecordingIfConsented(meetingTitle: String, meetingURL: URL) -> Bool {
|
|
|
|
|
+ let consentChoice = requestMeetingRecordingConsent(title: meetingTitle)
|
|
|
|
|
+ if consentChoice == .dismissed {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
meetingRecordingMonitorTask?.cancel()
|
|
meetingRecordingMonitorTask?.cancel()
|
|
|
meetingRecordingMonitorTask = nil
|
|
meetingRecordingMonitorTask = nil
|
|
|
if activeMeetingRecordingSession != nil { finishActiveMeetingRecording(cancelMonitoring: false) }
|
|
if activeMeetingRecordingSession != nil { finishActiveMeetingRecording(cancelMonitoring: false) }
|
|
|
- guard requestMeetingRecordingConsent(title: meetingTitle) else { return }
|
|
|
|
|
|
|
+ guard consentChoice == .allowAndContinue else { return true }
|
|
|
let permission = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
let permission = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
|
switch permission {
|
|
switch permission {
|
|
|
case .authorized:
|
|
case .authorized:
|
|
@@ -1548,6 +1688,7 @@ private extension ViewController {
|
|
|
@unknown default:
|
|
@unknown default:
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
+ return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func startAutomaticRecordingFlow(meetingTitle: String, meetingURL: URL) {
|
|
private func startAutomaticRecordingFlow(meetingTitle: String, meetingURL: URL) {
|
|
@@ -8858,7 +8999,8 @@ private extension ViewController {
|
|
|
|
|
|
|
|
private func openMeetingURL(_ url: URL, meeting: ScheduledMeeting? = nil) {
|
|
private func openMeetingURL(_ url: URL, meeting: ScheduledMeeting? = nil) {
|
|
|
let title = meeting?.title ?? "Scheduled Meeting"
|
|
let title = meeting?.title ?? "Scheduled Meeting"
|
|
|
- beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
|
|
|
|
|
|
|
+ let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
|
|
|
|
|
+ guard shouldOpenMeeting else { return }
|
|
|
NSWorkspace.shared.open(url)
|
|
NSWorkspace.shared.open(url)
|
|
|
}
|
|
}
|
|
|
|
|
|