Parcourir la Source

Improve meeting join consent modal controls and dismiss flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 il y a 1 mois
Parent
commit
9062f4251a
1 fichiers modifiés avec 149 ajouts et 7 suppressions
  1. 149 7
      meetings_app/ViewController.swift

+ 149 - 7
meetings_app/ViewController.swift

@@ -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