diff --git a/packages/frontend/apps/electron/resources/icons/tray-icon.png b/packages/frontend/apps/electron/resources/icons/tray-icon.png index 4385d9b11a..09ad658d2b 100644 Binary files a/packages/frontend/apps/electron/resources/icons/tray-icon.png and b/packages/frontend/apps/electron/resources/icons/tray-icon.png differ diff --git a/packages/frontend/apps/electron/src/main/recording/feature.ts b/packages/frontend/apps/electron/src/main/recording/feature.ts index ddb69a52a6..856624b33b 100644 --- a/packages/frontend/apps/electron/src/main/recording/feature.ts +++ b/packages/frontend/apps/electron/src/main/recording/feature.ts @@ -473,7 +473,7 @@ function setupMediaListeners() { // will be called when the app is ready or when the user has enabled the recording feature in settings export function setupRecordingFeature() { - if (!MeetingsSettingsState.value.enabled || !checkRecordingAvailable()) { + if (!MeetingsSettingsState.value.enabled || !checkCanRecordMeeting()) { return; } @@ -767,9 +767,24 @@ export const checkRecordingAvailable = () => { return (version.major === 14 && version.minor >= 2) || version.major > 14; }; -export const checkScreenRecordingPermission = () => { +export const checkMeetingPermissions = () => { if (!isMacOS()) { - return false; + return undefined; } - return systemPreferences.getMediaAccessStatus('screen') === 'granted'; + const mediaTypes = ['screen', 'microphone'] as const; + return Object.fromEntries( + mediaTypes.map(mediaType => [ + mediaType, + systemPreferences.getMediaAccessStatus(mediaType) === 'granted', + ]) + ) as Record<(typeof mediaTypes)[number], boolean>; +}; + +export const checkCanRecordMeeting = () => { + const features = checkMeetingPermissions(); + return ( + checkRecordingAvailable() && + features && + Object.values(features).every(feature => feature) + ); }; diff --git a/packages/frontend/apps/electron/src/main/recording/index.ts b/packages/frontend/apps/electron/src/main/recording/index.ts index 124ce89357..a057e93815 100644 --- a/packages/frontend/apps/electron/src/main/recording/index.ts +++ b/packages/frontend/apps/electron/src/main/recording/index.ts @@ -9,8 +9,8 @@ import { shell } from 'electron'; import { isMacOS } from '../../shared/utils'; import type { NamespaceHandlers } from '../type'; import { + checkMeetingPermissions, checkRecordingAvailable, - checkScreenRecordingPermission, disableRecordingFeature, getRawAudioBuffers, getRecording, @@ -73,13 +73,17 @@ export const recordingHandlers = { disableRecordingFeature: async () => { return disableRecordingFeature(); }, - checkScreenRecordingPermission: async () => { - return checkScreenRecordingPermission(); + checkMeetingPermissions: async () => { + return checkMeetingPermissions(); }, - showScreenRecordingPermissionSetting: async () => { + showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => { + const urlMap = { + screen: 'Privacy_ScreenCapture', + microphone: 'Privacy_Microphone', + }; if (isMacOS()) { return shell.openExternal( - 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture' + `x-apple.systempreferences:com.apple.preference.security?${urlMap[type]}` ); } // this only available on MacOS diff --git a/packages/frontend/apps/electron/src/main/tray/index.ts b/packages/frontend/apps/electron/src/main/tray/index.ts index cb11edcc71..4fd05748f4 100644 --- a/packages/frontend/apps/electron/src/main/tray/index.ts +++ b/packages/frontend/apps/electron/src/main/tray/index.ts @@ -15,8 +15,8 @@ import { beforeAppQuit } from '../cleanup'; import { logger } from '../logger'; import { appGroups$, + checkCanRecordMeeting, checkRecordingAvailable, - checkScreenRecordingPermission, MeetingsSettingsState, recordingStatus$, startRecording, @@ -89,7 +89,7 @@ class TrayState implements Disposable { // tray's icon icon: NativeImage = nativeImage .createFromPath(icons.tray) - .resize({ width: 16, height: 16 }); + .resize({ width: 18, height: 18 }); // tray's tooltip tooltip: string = 'AFFiNE'; @@ -142,10 +142,17 @@ class TrayState implements Disposable { const getConfig = () => { const items: TrayMenuConfig = []; - if ( - checkScreenRecordingPermission() && - MeetingsSettingsState.value.enabled - ) { + if (!MeetingsSettingsState.value.enabled) { + items.push({ + label: 'Meetings are disabled', + disabled: true, + }); + } else if (!checkCanRecordMeeting()) { + items.push({ + label: 'Required permissions not granted', + disabled: true, + }); + } else { const appGroups = appGroups$.value; const runningAppGroups = appGroups.filter( appGroup => appGroup.isRunning diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/meetings/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/meetings/index.tsx index 9b9569f34a..33528d606b 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/meetings/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/meetings/index.tsx @@ -75,6 +75,61 @@ const RecordingModeMenu = () => { ); }; +// Add the PermissionSettingRow component +interface PermissionSettingRowProps { + nameKey: string; + descriptionKey: string; + permissionSettingKey: string; + hasPermission: boolean; + onOpenPermissionSetting: () => void | Promise; +} + +const PermissionSettingRow = ({ + nameKey, + descriptionKey, + permissionSettingKey, + hasPermission, + onOpenPermissionSetting, +}: PermissionSettingRowProps) => { + const t = useI18n(); + + const handleClick = () => { + const result = onOpenPermissionSetting(); + if (result instanceof Promise) { + result.catch(error => { + console.error('Error opening permission setting:', error); + }); + } + }; + + return ( + + {t[descriptionKey]()} + {!hasPermission && ( + + {t[permissionSettingKey]()} + + )} + + } + > + + ) : ( + + ) + } + onClick={handleClick} + /> + + ); +}; + export const MeetingsSettings = () => { const t = useI18n(); const meetingSettingsService = useService(MeetingSettingsService); @@ -82,8 +137,10 @@ export const MeetingsSettings = () => { const [recordingFeatureAvailable, setRecordingFeatureAvailable] = useState(false); - const [screenRecordingPermission, setScreenRecordingPermission] = - useState(false); + const [permissions, setPermissions] = useState<{ + screen: boolean; + microphone: boolean; + }>(); const confirmModal = useConfirmModal(); @@ -97,9 +154,9 @@ export const MeetingsSettings = () => { setRecordingFeatureAvailable(false); }); meetingSettingsService - .checkScreenRecordingPermission() + .checkMeetingPermissions() .then(permission => { - setScreenRecordingPermission(permission ?? false); + setPermissions(permission); }) .catch(err => console.log(err)); }, [meetingSettingsService]); @@ -121,7 +178,9 @@ export const MeetingsSettings = () => { 'com.affine.settings.meetings.record.permission-modal.description' ](), onConfirm: async () => { - await meetingSettingsService.showScreenRecordingPermissionSetting(); + await meetingSettingsService.showRecordingPermissionSetting( + 'screen' + ); }, cancelText: t['com.affine.recording.dismiss'](), confirmButtonOptions: { @@ -146,7 +205,12 @@ export const MeetingsSettings = () => { const handleOpenScreenRecordingPermissionSetting = useAsyncCallback(async () => { - await meetingSettingsService.showScreenRecordingPermissionSetting(); + await meetingSettingsService.showRecordingPermissionSetting('screen'); + }, [meetingSettingsService]); + + const handleOpenMicrophoneRecordingPermissionSetting = + useAsyncCallback(async () => { + await meetingSettingsService.showRecordingPermissionSetting('microphone'); }, [meetingSettingsService]); const handleOpenSavedRecordings = useAsyncCallback(async () => { @@ -230,41 +294,24 @@ export const MeetingsSettings = () => { - - {t[ - 'com.affine.settings.meetings.privacy.screen-system-audio-recording.description' - ]()} - {!screenRecordingPermission && ( - - {t[ - 'com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting' - ]()} - - )} - + - - ) : ( - - ) - } - onClick={handleOpenScreenRecordingPermissionSetting} - /> - + /> + )} diff --git a/packages/frontend/core/src/modules/media/services/meeting-settings.ts b/packages/frontend/core/src/modules/media/services/meeting-settings.ts index d98f6647cb..490e0cbd70 100644 --- a/packages/frontend/core/src/modules/media/services/meeting-settings.ts +++ b/packages/frontend/core/src/modules/media/services/meeting-settings.ts @@ -103,13 +103,15 @@ export class MeetingSettingsService extends Service { return this.desktopApiService?.handler.recording.checkRecordingAvailable(); } - async checkScreenRecordingPermission() { - return this.desktopApiService?.handler.recording.checkScreenRecordingPermission(); + async checkMeetingPermissions() { + return this.desktopApiService?.handler.recording.checkMeetingPermissions(); } // the following methods are only available on MacOS right? - async showScreenRecordingPermissionSetting() { - return this.desktopApiService?.handler.recording.showScreenRecordingPermissionSetting(); + async showRecordingPermissionSetting(type: 'screen' | 'microphone') { + return this.desktopApiService?.handler.recording.showRecordingPermissionSetting( + type + ); } setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => { diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 97e48a5224..446acdd859 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -5411,6 +5411,18 @@ export function useAFFiNEI18N(): { * `Click to allow` */ ["com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"](): string; + /** + * `Microphone` + */ + ["com.affine.settings.meetings.privacy.microphone"](): string; + /** + * `The Meeting feature requires permission to be used.` + */ + ["com.affine.settings.meetings.privacy.microphone.description"](): string; + /** + * `Click to allow` + */ + ["com.affine.settings.meetings.privacy.microphone.permission-setting"](): string; /** * `Do nothing` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index fe8bc9a396..5c5fe4aefc 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1350,6 +1350,9 @@ "com.affine.settings.meetings.privacy.screen-system-audio-recording": "Screen & System audio recording", "com.affine.settings.meetings.privacy.screen-system-audio-recording.description": "The Meeting feature requires permission to be used.", "com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting": "Click to allow", + "com.affine.settings.meetings.privacy.microphone": "Microphone", + "com.affine.settings.meetings.privacy.microphone.description": "The Meeting feature requires permission to be used.", + "com.affine.settings.meetings.privacy.microphone.permission-setting": "Click to allow", "com.affine.settings.meetings.record.recording-mode.none": "Do nothing", "com.affine.settings.meetings.record.recording-mode.auto-start": "Auto start recording", "com.affine.settings.meetings.record.recording-mode.prompt": "Show a recording prompt",