fix(electron): optimize meeting privacy settings (#12530)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for requesting screen recording permission on macOS in addition to microphone permission.
  - Introduced a new "Permission issues" section in meeting privacy settings, including a button to restart the app if permission status is not updated.
- **Improvements**
  - Unified permission handling for screen and microphone settings, simplifying the user experience.
  - Added new localized strings for enhanced clarity regarding permission issues and app restart instructions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
pengx17
2025-05-27 11:08:06 +00:00
parent d6476db64d
commit 4ad008f712
6 changed files with 74 additions and 29 deletions

View File

@@ -471,6 +471,21 @@ function setupMediaListeners() {
); );
} }
function askForScreenRecordingPermission() {
if (!isMacOS()) {
return false;
}
try {
const ShareableContent = require('@affine/native').ShareableContent;
// this will trigger the permission prompt
new ShareableContent();
return true;
} catch (error) {
logger.error('failed to ask for screen recording permission', error);
}
return false;
}
// will be called when the app is ready or when the user has enabled the recording feature in settings // will be called when the app is ready or when the user has enabled the recording feature in settings
export function setupRecordingFeature() { export function setupRecordingFeature() {
if (!MeetingsSettingsState.value.enabled || !checkCanRecordMeeting()) { if (!MeetingsSettingsState.value.enabled || !checkCanRecordMeeting()) {
@@ -788,10 +803,15 @@ export const checkMeetingPermissions = () => {
) as Record<(typeof mediaTypes)[number], boolean>; ) as Record<(typeof mediaTypes)[number], boolean>;
}; };
export const askForMeetingPermission = async (type: 'microphone') => { export const askForMeetingPermission = async (
type: 'microphone' | 'screen'
) => {
if (!isMacOS()) { if (!isMacOS()) {
return false; return false;
} }
if (type === 'screen') {
return askForScreenRecordingPermission();
}
return systemPreferences.askForMediaAccess(type); return systemPreferences.askForMediaAccess(type);
}; };

View File

@@ -77,18 +77,17 @@ export const recordingHandlers = {
checkMeetingPermissions: async () => { checkMeetingPermissions: async () => {
return checkMeetingPermissions(); return checkMeetingPermissions();
}, },
askForMeetingPermission: async (_, type: 'microphone') => { askForMeetingPermission: async (_, type: 'screen' | 'microphone') => {
return askForMeetingPermission(type); return askForMeetingPermission(type);
}, },
showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => { showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => {
if (isMacOS()) {
const urlMap = { const urlMap = {
screen: 'Privacy_ScreenCapture', screen: 'Privacy_ScreenCapture',
microphone: 'Privacy_Microphone', microphone: 'Privacy_Microphone',
}; };
if (isMacOS()) { const url = `x-apple.systempreferences:com.apple.preference.security?${urlMap[type]}`;
return shell.openExternal( return shell.openExternal(url);
`x-apple.systempreferences:com.apple.preference.security?${urlMap[type]}`
);
} }
// this only available on MacOS // this only available on MacOS
return false; return false;

View File

@@ -1,4 +1,5 @@
import { import {
Button,
IconButton, IconButton,
Menu, Menu,
MenuItem, MenuItem,
@@ -11,6 +12,7 @@ import {
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { DesktopApiService } from '@affine/core/modules/desktop-api';
import { MeetingSettingsService } from '@affine/core/modules/media/services/meeting-settings'; import { MeetingSettingsService } from '@affine/core/modules/media/services/meeting-settings';
import type { MeetingSettingsSchema } from '@affine/electron/main/shared-state-schema'; import type { MeetingSettingsSchema } from '@affine/electron/main/shared-state-schema';
import { Trans, useI18n } from '@affine/i18n'; import { Trans, useI18n } from '@affine/i18n';
@@ -133,6 +135,7 @@ const PermissionSettingRow = ({
const MeetingsSettingsMain = () => { const MeetingsSettingsMain = () => {
const t = useI18n(); const t = useI18n();
const meetingSettingsService = useService(MeetingSettingsService); const meetingSettingsService = useService(MeetingSettingsService);
const desktopApiService = useService(DesktopApiService);
const settings = useLiveData(meetingSettingsService.settings$); const settings = useLiveData(meetingSettingsService.settings$);
const [recordingFeatureAvailable, setRecordingFeatureAvailable] = const [recordingFeatureAvailable, setRecordingFeatureAvailable] =
useState(false); useState(false);
@@ -175,26 +178,22 @@ const MeetingsSettingsMain = () => {
[meetingSettingsService] [meetingSettingsService]
); );
const handleOpenScreenRecordingPermissionSetting = const handleOpenPermissionSetting = useAsyncCallback(
useAsyncCallback(async () => { async (type: 'screen' | 'microphone') => {
await meetingSettingsService.showRecordingPermissionSetting('screen'); await meetingSettingsService.askForMeetingPermission(type);
}, [meetingSettingsService]); await meetingSettingsService.showRecordingPermissionSetting(type);
},
const handleOpenMicrophoneRecordingPermissionSetting = [meetingSettingsService]
useAsyncCallback(async () => {
const result =
await meetingSettingsService.askForMeetingPermission('microphone');
if (!result) {
await meetingSettingsService.showRecordingPermissionSetting(
'microphone'
); );
}
}, [meetingSettingsService]);
const handleOpenSavedRecordings = useAsyncCallback(async () => { const handleOpenSavedRecordings = useAsyncCallback(async () => {
await meetingSettingsService.openSavedRecordings(); await meetingSettingsService.openSavedRecordings();
}, [meetingSettingsService]); }, [meetingSettingsService]);
const handleRestartApp = useAsyncCallback(async () => {
await desktopApiService.handler.ui.restartApp();
}, [desktopApiService]);
return ( return (
<div className={styles.meetingWrapper}> <div className={styles.meetingWrapper}>
<SettingHeader <SettingHeader
@@ -304,8 +303,8 @@ const MeetingsSettingsMain = () => {
descriptionKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.description" descriptionKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.description"
permissionSettingKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting" permissionSettingKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"
hasPermission={permissions?.screen || false} hasPermission={permissions?.screen || false}
onOpenPermissionSetting={ onOpenPermissionSetting={() =>
handleOpenScreenRecordingPermissionSetting handleOpenPermissionSetting('screen')
} }
/> />
<PermissionSettingRow <PermissionSettingRow
@@ -313,11 +312,23 @@ const MeetingsSettingsMain = () => {
descriptionKey="com.affine.settings.meetings.privacy.microphone.description" descriptionKey="com.affine.settings.meetings.privacy.microphone.description"
permissionSettingKey="com.affine.settings.meetings.privacy.microphone.permission-setting" permissionSettingKey="com.affine.settings.meetings.privacy.microphone.permission-setting"
hasPermission={permissions?.microphone || false} hasPermission={permissions?.microphone || false}
onOpenPermissionSetting={ onOpenPermissionSetting={() =>
handleOpenMicrophoneRecordingPermissionSetting handleOpenPermissionSetting('microphone')
} }
/> />
</SettingWrapper> </SettingWrapper>
<SettingWrapper>
<SettingRow
name={t['com.affine.settings.meetings.privacy.issues']()}
desc={t[
'com.affine.settings.meetings.privacy.issues.description'
]()}
>
<Button onClick={handleRestartApp}>
{t['com.affine.settings.meetings.privacy.issues.restart']()}
</Button>
</SettingRow>
</SettingWrapper>
</> </>
)} )}
</div> </div>

View File

@@ -130,7 +130,7 @@ export class MeetingSettingsService extends Service {
); );
} }
async askForMeetingPermission(type: 'microphone') { async askForMeetingPermission(type: 'microphone' | 'screen') {
return this.desktopApiService?.handler.recording.askForMeetingPermission( return this.desktopApiService?.handler.recording.askForMeetingPermission(
type type
); );

View File

@@ -5598,6 +5598,18 @@ export function useAFFiNEI18N(): {
* `Click to allow` * `Click to allow`
*/ */
["com.affine.settings.meetings.privacy.microphone.permission-setting"](): string; ["com.affine.settings.meetings.privacy.microphone.permission-setting"](): string;
/**
* `Permission issues`
*/
["com.affine.settings.meetings.privacy.issues"](): string;
/**
* `Permissions are granted but the status isn't updated? Restart the app to refresh permissions.`
*/
["com.affine.settings.meetings.privacy.issues.description"](): string;
/**
* `Restart App`
*/
["com.affine.settings.meetings.privacy.issues.restart"](): string;
/** /**
* `Do nothing` * `Do nothing`
*/ */

View File

@@ -1398,6 +1398,9 @@
"com.affine.settings.meetings.privacy.microphone": "Microphone", "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.description": "The Meeting feature requires permission to be used.",
"com.affine.settings.meetings.privacy.microphone.permission-setting": "Click to allow", "com.affine.settings.meetings.privacy.microphone.permission-setting": "Click to allow",
"com.affine.settings.meetings.privacy.issues": "Permission issues",
"com.affine.settings.meetings.privacy.issues.description": "Permissions are granted but the status isn't updated? Restart the app to refresh permissions.",
"com.affine.settings.meetings.privacy.issues.restart": "Restart App",
"com.affine.settings.meetings.record.recording-mode.none": "Do nothing", "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.auto-start": "Auto start recording",
"com.affine.settings.meetings.record.recording-mode.prompt": "Show a recording prompt", "com.affine.settings.meetings.record.recording-mode.prompt": "Show a recording prompt",