|
|
@@ -509,6 +509,8 @@ final class ViewController: NSViewController {
|
|
509
|
509
|
private let aiCompanionLocalRecordingsDefaultsKey = "aiCompanion.localRecordings"
|
|
510
|
510
|
private let aiCompanionPreferredLanguage1DefaultsKey = "aiCompanion.preferredLanguage1"
|
|
511
|
511
|
private let aiCompanionPreferredLanguage2DefaultsKey = "aiCompanion.preferredLanguage2"
|
|
|
512
|
+ private weak var activeMeetingConsentWindow: NSWindow?
|
|
|
513
|
+ private var didTapInlineMeetingConsentClose = false
|
|
512
|
514
|
private let ratingEligibleUsageSeconds: TimeInterval = 30 * 60
|
|
513
|
515
|
private let meetingTranscriptionService = MeetingTranscriptionService()
|
|
514
|
516
|
private let meetingNotesService = MeetingNotesService()
|
|
|
@@ -876,7 +878,8 @@ private extension ViewController {
|
|
876
|
878
|
return
|
|
877
|
879
|
}
|
|
878
|
880
|
|
|
879
|
|
- beginMeetingRecordingIfConsented(meetingTitle: "Quick Join Meeting", meetingURL: url)
|
|
|
881
|
+ let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: "Quick Join Meeting", meetingURL: url)
|
|
|
882
|
+ guard shouldOpenMeeting else { return }
|
|
880
|
883
|
openInDefaultBrowser(url: url)
|
|
881
|
884
|
consumeNonPremiumJoinTrialIfNeeded()
|
|
882
|
885
|
}
|
|
|
@@ -1319,15 +1322,147 @@ private extension ViewController {
|
|
1319
|
1322
|
alert.runModal()
|
|
1320
|
1323
|
}
|
|
1321
|
1324
|
|
|
1322
|
|
- private func requestMeetingRecordingConsent(title: String) -> Bool {
|
|
|
1325
|
+ private enum MeetingRecordingConsentChoice {
|
|
|
1326
|
+ case allowAndContinue
|
|
|
1327
|
+ case continueWithoutRecording
|
|
|
1328
|
+ case dismissed
|
|
|
1329
|
+ }
|
|
|
1330
|
+
|
|
|
1331
|
+ private func requestMeetingRecordingConsent(title: String) -> MeetingRecordingConsentChoice {
|
|
1323
|
1332
|
let alert = NSAlert()
|
|
1324
|
1333
|
alert.messageText = "Record this meeting locally?"
|
|
1325
|
1334
|
alert.informativeText = "With your consent, the app will record meeting audio on this Mac and show it in AI Companion after the meeting."
|
|
1326
|
1335
|
alert.addButton(withTitle: "Allow and Continue")
|
|
1327
|
1336
|
alert.addButton(withTitle: "Continue Without Recording")
|
|
|
1337
|
+ alert.window.title = title
|
|
|
1338
|
+
|
|
|
1339
|
+ let speechOptions = aiCompanionSupportedSpeechLocaleOptions()
|
|
|
1340
|
+ let languageContainer = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 184))
|
|
|
1341
|
+ languageContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1342
|
+ languageContainer.widthAnchor.constraint(equalToConstant: 360).isActive = true
|
|
|
1343
|
+ languageContainer.heightAnchor.constraint(equalToConstant: 184).isActive = true
|
|
|
1344
|
+
|
|
|
1345
|
+ let languageTitle = NSTextField(labelWithString: "Transcription Languages")
|
|
|
1346
|
+ languageTitle.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1347
|
+ languageTitle.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
1348
|
+ languageTitle.textColor = NSColor.secondaryLabelColor
|
|
|
1349
|
+
|
|
|
1350
|
+ let language1Popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
1351
|
+ language1Popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1352
|
+ language1Popup.controlSize = .regular
|
|
|
1353
|
+ for option in speechOptions {
|
|
|
1354
|
+ language1Popup.addItem(withTitle: option.displayName)
|
|
|
1355
|
+ language1Popup.lastItem?.representedObject = option.identifier
|
|
|
1356
|
+ }
|
|
|
1357
|
+
|
|
|
1358
|
+ let selectedPrimary = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage1DefaultsKey)
|
|
|
1359
|
+ ?? speechOptions.first?.identifier
|
|
|
1360
|
+ ?? Locale.current.identifier
|
|
|
1361
|
+ selectSettingsPageLanguage(identifier: selectedPrimary, in: language1Popup)
|
|
|
1362
|
+
|
|
|
1363
|
+ let language2Popup = NSPopUpButton(frame: .zero, pullsDown: false)
|
|
|
1364
|
+ language2Popup.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1365
|
+ language2Popup.controlSize = .regular
|
|
|
1366
|
+ language2Popup.addItem(withTitle: "None")
|
|
|
1367
|
+ language2Popup.lastItem?.representedObject = ""
|
|
|
1368
|
+ for option in speechOptions {
|
|
|
1369
|
+ language2Popup.addItem(withTitle: option.displayName)
|
|
|
1370
|
+ language2Popup.lastItem?.representedObject = option.identifier
|
|
|
1371
|
+ }
|
|
|
1372
|
+ if let selectedSecondary = UserDefaults.standard.string(forKey: aiCompanionPreferredLanguage2DefaultsKey),
|
|
|
1373
|
+ selectedSecondary.isEmpty == false {
|
|
|
1374
|
+ selectSettingsPageLanguage(identifier: selectedSecondary, in: language2Popup)
|
|
|
1375
|
+ } else {
|
|
|
1376
|
+ language2Popup.selectItem(at: 0)
|
|
|
1377
|
+ }
|
|
|
1378
|
+
|
|
|
1379
|
+ let language1Label = NSTextField(labelWithString: "Preferred Language 1")
|
|
|
1380
|
+ language1Label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1381
|
+ language1Label.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
1382
|
+ language1Label.textColor = NSColor.secondaryLabelColor
|
|
|
1383
|
+ language1Label.alignment = .center
|
|
|
1384
|
+
|
|
|
1385
|
+ let language2Label = NSTextField(labelWithString: "Preferred Language 2")
|
|
|
1386
|
+ language2Label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1387
|
+ language2Label.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
1388
|
+ language2Label.textColor = NSColor.secondaryLabelColor
|
|
|
1389
|
+ language2Label.alignment = .center
|
|
|
1390
|
+
|
|
|
1391
|
+ languageContainer.addSubview(languageTitle)
|
|
|
1392
|
+ languageContainer.addSubview(language1Label)
|
|
|
1393
|
+ languageContainer.addSubview(language1Popup)
|
|
|
1394
|
+ languageContainer.addSubview(language2Label)
|
|
|
1395
|
+ languageContainer.addSubview(language2Popup)
|
|
|
1396
|
+
|
|
|
1397
|
+ NSLayoutConstraint.activate([
|
|
|
1398
|
+ languageTitle.topAnchor.constraint(equalTo: languageContainer.topAnchor, constant: 4),
|
|
|
1399
|
+ languageTitle.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
1400
|
+
|
|
|
1401
|
+ language1Label.topAnchor.constraint(equalTo: languageTitle.bottomAnchor, constant: 12),
|
|
|
1402
|
+ language1Label.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
1403
|
+ language1Popup.topAnchor.constraint(equalTo: language1Label.bottomAnchor, constant: 4),
|
|
|
1404
|
+ language1Popup.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
1405
|
+ language1Popup.widthAnchor.constraint(equalToConstant: 280),
|
|
|
1406
|
+
|
|
|
1407
|
+ language2Label.topAnchor.constraint(equalTo: language1Popup.bottomAnchor, constant: 10),
|
|
|
1408
|
+ language2Label.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
1409
|
+ language2Popup.topAnchor.constraint(equalTo: language2Label.bottomAnchor, constant: 4),
|
|
|
1410
|
+ language2Popup.centerXAnchor.constraint(equalTo: languageContainer.centerXAnchor),
|
|
|
1411
|
+ language2Popup.widthAnchor.constraint(equalToConstant: 280),
|
|
|
1412
|
+ language2Popup.bottomAnchor.constraint(lessThanOrEqualTo: languageContainer.bottomAnchor, constant: -4)
|
|
|
1413
|
+ ])
|
|
|
1414
|
+
|
|
|
1415
|
+ alert.accessoryView = languageContainer
|
|
|
1416
|
+ didTapInlineMeetingConsentClose = false
|
|
|
1417
|
+ activeMeetingConsentWindow = alert.window
|
|
|
1418
|
+ if let contentView = alert.window.contentView {
|
|
|
1419
|
+ let topCloseButton = NSButton(title: "✕", target: self, action: #selector(meetingConsentInlineCloseTapped(_:)))
|
|
|
1420
|
+ topCloseButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
1421
|
+ topCloseButton.isBordered = false
|
|
|
1422
|
+ topCloseButton.font = NSFont.systemFont(ofSize: 15, weight: .semibold)
|
|
|
1423
|
+ topCloseButton.contentTintColor = NSColor.tertiaryLabelColor
|
|
|
1424
|
+ topCloseButton.wantsLayer = true
|
|
|
1425
|
+ topCloseButton.layer?.cornerRadius = 20
|
|
|
1426
|
+ topCloseButton.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.35).cgColor
|
|
|
1427
|
+ contentView.addSubview(topCloseButton)
|
|
|
1428
|
+ NSLayoutConstraint.activate([
|
|
|
1429
|
+ topCloseButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
|
|
1430
|
+ topCloseButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
|
|
|
1431
|
+ topCloseButton.widthAnchor.constraint(equalToConstant: 40),
|
|
|
1432
|
+ topCloseButton.heightAnchor.constraint(equalToConstant: 40)
|
|
|
1433
|
+ ])
|
|
|
1434
|
+ }
|
|
1328
|
1435
|
let response = alert.runModal()
|
|
1329
|
|
- if response == .alertFirstButtonReturn { return true }
|
|
1330
|
|
- return false
|
|
|
1436
|
+ activeMeetingConsentWindow = nil
|
|
|
1437
|
+
|
|
|
1438
|
+ if didTapInlineMeetingConsentClose {
|
|
|
1439
|
+ return .dismissed
|
|
|
1440
|
+ }
|
|
|
1441
|
+
|
|
|
1442
|
+ if let primary = language1Popup.selectedItem?.representedObject as? String,
|
|
|
1443
|
+ primary.isEmpty == false {
|
|
|
1444
|
+ var secondary = language2Popup.selectedItem?.representedObject as? String
|
|
|
1445
|
+ if secondary?.isEmpty == true {
|
|
|
1446
|
+ secondary = nil
|
|
|
1447
|
+ }
|
|
|
1448
|
+ updateAiCompanionPreferredSpeechLanguages(primary: primary, secondary: secondary)
|
|
|
1449
|
+ }
|
|
|
1450
|
+
|
|
|
1451
|
+ switch response {
|
|
|
1452
|
+ case .alertFirstButtonReturn:
|
|
|
1453
|
+ return .allowAndContinue
|
|
|
1454
|
+ case .alertSecondButtonReturn:
|
|
|
1455
|
+ return .continueWithoutRecording
|
|
|
1456
|
+ default:
|
|
|
1457
|
+ return .dismissed
|
|
|
1458
|
+ }
|
|
|
1459
|
+ }
|
|
|
1460
|
+
|
|
|
1461
|
+ @objc private func meetingConsentInlineCloseTapped(_ sender: NSButton) {
|
|
|
1462
|
+ didTapInlineMeetingConsentClose = true
|
|
|
1463
|
+ guard let consentWindow = activeMeetingConsentWindow else { return }
|
|
|
1464
|
+ NSApp.stopModal(withCode: .abort)
|
|
|
1465
|
+ consentWindow.orderOut(nil)
|
|
1331
|
1466
|
}
|
|
1332
|
1467
|
|
|
1333
|
1468
|
private func loadAiCompanionLocalRecordings() {
|
|
|
@@ -1523,11 +1658,16 @@ private extension ViewController {
|
|
1523
|
1658
|
return directory
|
|
1524
|
1659
|
}
|
|
1525
|
1660
|
|
|
1526
|
|
- private func beginMeetingRecordingIfConsented(meetingTitle: String, meetingURL: URL) {
|
|
|
1661
|
+ private func beginMeetingRecordingIfConsented(meetingTitle: String, meetingURL: URL) -> Bool {
|
|
|
1662
|
+ let consentChoice = requestMeetingRecordingConsent(title: meetingTitle)
|
|
|
1663
|
+ if consentChoice == .dismissed {
|
|
|
1664
|
+ return false
|
|
|
1665
|
+ }
|
|
|
1666
|
+
|
|
1527
|
1667
|
meetingRecordingMonitorTask?.cancel()
|
|
1528
|
1668
|
meetingRecordingMonitorTask = nil
|
|
1529
|
1669
|
if activeMeetingRecordingSession != nil { finishActiveMeetingRecording(cancelMonitoring: false) }
|
|
1530
|
|
- guard requestMeetingRecordingConsent(title: meetingTitle) else { return }
|
|
|
1670
|
+ guard consentChoice == .allowAndContinue else { return true }
|
|
1531
|
1671
|
let permission = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
1532
|
1672
|
switch permission {
|
|
1533
|
1673
|
case .authorized:
|
|
|
@@ -1548,6 +1688,7 @@ private extension ViewController {
|
|
1548
|
1688
|
@unknown default:
|
|
1549
|
1689
|
break
|
|
1550
|
1690
|
}
|
|
|
1691
|
+ return true
|
|
1551
|
1692
|
}
|
|
1552
|
1693
|
|
|
1553
|
1694
|
private func startAutomaticRecordingFlow(meetingTitle: String, meetingURL: URL) {
|
|
|
@@ -8858,7 +8999,8 @@ private extension ViewController {
|
|
8858
|
8999
|
|
|
8859
|
9000
|
private func openMeetingURL(_ url: URL, meeting: ScheduledMeeting? = nil) {
|
|
8860
|
9001
|
let title = meeting?.title ?? "Scheduled Meeting"
|
|
8861
|
|
- beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
|
|
|
9002
|
+ let shouldOpenMeeting = beginMeetingRecordingIfConsented(meetingTitle: title, meetingURL: url)
|
|
|
9003
|
+ guard shouldOpenMeeting else { return }
|
|
8862
|
9004
|
NSWorkspace.shared.open(url)
|
|
8863
|
9005
|
}
|
|
8864
|
9006
|
|