feat(electron): meeting recording permissions checks (#11505)
fix AF-2472, AF-2446  
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 939 B |
@@ -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
|
// 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 || !checkRecordingAvailable()) {
|
if (!MeetingsSettingsState.value.enabled || !checkCanRecordMeeting()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,9 +767,24 @@ export const checkRecordingAvailable = () => {
|
|||||||
return (version.major === 14 && version.minor >= 2) || version.major > 14;
|
return (version.major === 14 && version.minor >= 2) || version.major > 14;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkScreenRecordingPermission = () => {
|
export const checkMeetingPermissions = () => {
|
||||||
if (!isMacOS()) {
|
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)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { shell } from 'electron';
|
|||||||
import { isMacOS } from '../../shared/utils';
|
import { isMacOS } from '../../shared/utils';
|
||||||
import type { NamespaceHandlers } from '../type';
|
import type { NamespaceHandlers } from '../type';
|
||||||
import {
|
import {
|
||||||
|
checkMeetingPermissions,
|
||||||
checkRecordingAvailable,
|
checkRecordingAvailable,
|
||||||
checkScreenRecordingPermission,
|
|
||||||
disableRecordingFeature,
|
disableRecordingFeature,
|
||||||
getRawAudioBuffers,
|
getRawAudioBuffers,
|
||||||
getRecording,
|
getRecording,
|
||||||
@@ -73,13 +73,17 @@ export const recordingHandlers = {
|
|||||||
disableRecordingFeature: async () => {
|
disableRecordingFeature: async () => {
|
||||||
return disableRecordingFeature();
|
return disableRecordingFeature();
|
||||||
},
|
},
|
||||||
checkScreenRecordingPermission: async () => {
|
checkMeetingPermissions: async () => {
|
||||||
return checkScreenRecordingPermission();
|
return checkMeetingPermissions();
|
||||||
},
|
},
|
||||||
showScreenRecordingPermissionSetting: async () => {
|
showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => {
|
||||||
|
const urlMap = {
|
||||||
|
screen: 'Privacy_ScreenCapture',
|
||||||
|
microphone: 'Privacy_Microphone',
|
||||||
|
};
|
||||||
if (isMacOS()) {
|
if (isMacOS()) {
|
||||||
return shell.openExternal(
|
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
|
// this only available on MacOS
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import { beforeAppQuit } from '../cleanup';
|
|||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import {
|
import {
|
||||||
appGroups$,
|
appGroups$,
|
||||||
|
checkCanRecordMeeting,
|
||||||
checkRecordingAvailable,
|
checkRecordingAvailable,
|
||||||
checkScreenRecordingPermission,
|
|
||||||
MeetingsSettingsState,
|
MeetingsSettingsState,
|
||||||
recordingStatus$,
|
recordingStatus$,
|
||||||
startRecording,
|
startRecording,
|
||||||
@@ -89,7 +89,7 @@ class TrayState implements Disposable {
|
|||||||
// tray's icon
|
// tray's icon
|
||||||
icon: NativeImage = nativeImage
|
icon: NativeImage = nativeImage
|
||||||
.createFromPath(icons.tray)
|
.createFromPath(icons.tray)
|
||||||
.resize({ width: 16, height: 16 });
|
.resize({ width: 18, height: 18 });
|
||||||
|
|
||||||
// tray's tooltip
|
// tray's tooltip
|
||||||
tooltip: string = 'AFFiNE';
|
tooltip: string = 'AFFiNE';
|
||||||
@@ -142,10 +142,17 @@ class TrayState implements Disposable {
|
|||||||
|
|
||||||
const getConfig = () => {
|
const getConfig = () => {
|
||||||
const items: TrayMenuConfig = [];
|
const items: TrayMenuConfig = [];
|
||||||
if (
|
if (!MeetingsSettingsState.value.enabled) {
|
||||||
checkScreenRecordingPermission() &&
|
items.push({
|
||||||
MeetingsSettingsState.value.enabled
|
label: 'Meetings are disabled',
|
||||||
) {
|
disabled: true,
|
||||||
|
});
|
||||||
|
} else if (!checkCanRecordMeeting()) {
|
||||||
|
items.push({
|
||||||
|
label: 'Required permissions not granted',
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
const appGroups = appGroups$.value;
|
const appGroups = appGroups$.value;
|
||||||
const runningAppGroups = appGroups.filter(
|
const runningAppGroups = appGroups.filter(
|
||||||
appGroup => appGroup.isRunning
|
appGroup => appGroup.isRunning
|
||||||
|
|||||||
@@ -75,6 +75,61 @@ const RecordingModeMenu = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add the PermissionSettingRow component
|
||||||
|
interface PermissionSettingRowProps {
|
||||||
|
nameKey: string;
|
||||||
|
descriptionKey: string;
|
||||||
|
permissionSettingKey: string;
|
||||||
|
hasPermission: boolean;
|
||||||
|
onOpenPermissionSetting: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<SettingRow
|
||||||
|
name={t[nameKey]()}
|
||||||
|
desc={
|
||||||
|
<>
|
||||||
|
{t[descriptionKey]()}
|
||||||
|
{!hasPermission && (
|
||||||
|
<span onClick={handleClick} className={styles.permissionSetting}>
|
||||||
|
{t[permissionSettingKey]()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon={
|
||||||
|
hasPermission ? (
|
||||||
|
<DoneIcon />
|
||||||
|
) : (
|
||||||
|
<InformationFillDuotoneIcon className={styles.noPermissionIcon} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const MeetingsSettings = () => {
|
export const MeetingsSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const meetingSettingsService = useService(MeetingSettingsService);
|
const meetingSettingsService = useService(MeetingSettingsService);
|
||||||
@@ -82,8 +137,10 @@ export const MeetingsSettings = () => {
|
|||||||
const [recordingFeatureAvailable, setRecordingFeatureAvailable] =
|
const [recordingFeatureAvailable, setRecordingFeatureAvailable] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const [screenRecordingPermission, setScreenRecordingPermission] =
|
const [permissions, setPermissions] = useState<{
|
||||||
useState(false);
|
screen: boolean;
|
||||||
|
microphone: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const confirmModal = useConfirmModal();
|
const confirmModal = useConfirmModal();
|
||||||
|
|
||||||
@@ -97,9 +154,9 @@ export const MeetingsSettings = () => {
|
|||||||
setRecordingFeatureAvailable(false);
|
setRecordingFeatureAvailable(false);
|
||||||
});
|
});
|
||||||
meetingSettingsService
|
meetingSettingsService
|
||||||
.checkScreenRecordingPermission()
|
.checkMeetingPermissions()
|
||||||
.then(permission => {
|
.then(permission => {
|
||||||
setScreenRecordingPermission(permission ?? false);
|
setPermissions(permission);
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err));
|
||||||
}, [meetingSettingsService]);
|
}, [meetingSettingsService]);
|
||||||
@@ -121,7 +178,9 @@ export const MeetingsSettings = () => {
|
|||||||
'com.affine.settings.meetings.record.permission-modal.description'
|
'com.affine.settings.meetings.record.permission-modal.description'
|
||||||
](),
|
](),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
await meetingSettingsService.showScreenRecordingPermissionSetting();
|
await meetingSettingsService.showRecordingPermissionSetting(
|
||||||
|
'screen'
|
||||||
|
);
|
||||||
},
|
},
|
||||||
cancelText: t['com.affine.recording.dismiss'](),
|
cancelText: t['com.affine.recording.dismiss'](),
|
||||||
confirmButtonOptions: {
|
confirmButtonOptions: {
|
||||||
@@ -146,7 +205,12 @@ export const MeetingsSettings = () => {
|
|||||||
|
|
||||||
const handleOpenScreenRecordingPermissionSetting =
|
const handleOpenScreenRecordingPermissionSetting =
|
||||||
useAsyncCallback(async () => {
|
useAsyncCallback(async () => {
|
||||||
await meetingSettingsService.showScreenRecordingPermissionSetting();
|
await meetingSettingsService.showRecordingPermissionSetting('screen');
|
||||||
|
}, [meetingSettingsService]);
|
||||||
|
|
||||||
|
const handleOpenMicrophoneRecordingPermissionSetting =
|
||||||
|
useAsyncCallback(async () => {
|
||||||
|
await meetingSettingsService.showRecordingPermissionSetting('microphone');
|
||||||
}, [meetingSettingsService]);
|
}, [meetingSettingsService]);
|
||||||
|
|
||||||
const handleOpenSavedRecordings = useAsyncCallback(async () => {
|
const handleOpenSavedRecordings = useAsyncCallback(async () => {
|
||||||
@@ -230,41 +294,24 @@ export const MeetingsSettings = () => {
|
|||||||
<SettingWrapper
|
<SettingWrapper
|
||||||
title={t['com.affine.settings.meetings.privacy.header']()}
|
title={t['com.affine.settings.meetings.privacy.header']()}
|
||||||
>
|
>
|
||||||
<SettingRow
|
<PermissionSettingRow
|
||||||
name={t[
|
nameKey="com.affine.settings.meetings.privacy.screen-system-audio-recording"
|
||||||
'com.affine.settings.meetings.privacy.screen-system-audio-recording'
|
descriptionKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.description"
|
||||||
]()}
|
permissionSettingKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"
|
||||||
desc={
|
hasPermission={permissions?.screen || false}
|
||||||
<>
|
onOpenPermissionSetting={
|
||||||
{t[
|
handleOpenScreenRecordingPermissionSetting
|
||||||
'com.affine.settings.meetings.privacy.screen-system-audio-recording.description'
|
|
||||||
]()}
|
|
||||||
{!screenRecordingPermission && (
|
|
||||||
<span
|
|
||||||
onClick={handleOpenScreenRecordingPermissionSetting}
|
|
||||||
className={styles.permissionSetting}
|
|
||||||
>
|
|
||||||
{t[
|
|
||||||
'com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting'
|
|
||||||
]()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
icon={
|
|
||||||
screenRecordingPermission ? (
|
|
||||||
<DoneIcon />
|
|
||||||
) : (
|
|
||||||
<InformationFillDuotoneIcon
|
|
||||||
className={styles.noPermissionIcon}
|
|
||||||
/>
|
/>
|
||||||
)
|
<PermissionSettingRow
|
||||||
|
nameKey="com.affine.settings.meetings.privacy.microphone"
|
||||||
|
descriptionKey="com.affine.settings.meetings.privacy.microphone.description"
|
||||||
|
permissionSettingKey="com.affine.settings.meetings.privacy.microphone.permission-setting"
|
||||||
|
hasPermission={permissions?.microphone || false}
|
||||||
|
onOpenPermissionSetting={
|
||||||
|
handleOpenMicrophoneRecordingPermissionSetting
|
||||||
}
|
}
|
||||||
onClick={handleOpenScreenRecordingPermissionSetting}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -103,13 +103,15 @@ export class MeetingSettingsService extends Service {
|
|||||||
return this.desktopApiService?.handler.recording.checkRecordingAvailable();
|
return this.desktopApiService?.handler.recording.checkRecordingAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkScreenRecordingPermission() {
|
async checkMeetingPermissions() {
|
||||||
return this.desktopApiService?.handler.recording.checkScreenRecordingPermission();
|
return this.desktopApiService?.handler.recording.checkMeetingPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// the following methods are only available on MacOS right?
|
// the following methods are only available on MacOS right?
|
||||||
async showScreenRecordingPermissionSetting() {
|
async showRecordingPermissionSetting(type: 'screen' | 'microphone') {
|
||||||
return this.desktopApiService?.handler.recording.showScreenRecordingPermissionSetting();
|
return this.desktopApiService?.handler.recording.showRecordingPermissionSetting(
|
||||||
|
type
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => {
|
setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => {
|
||||||
|
|||||||
@@ -5411,6 +5411,18 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Click to allow`
|
* `Click to allow`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"](): string;
|
["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`
|
* `Do nothing`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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": "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.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.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.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",
|
||||||
|
|||||||
Reference in New Issue
Block a user