pengx17
2025-04-08 10:18:08 +00:00
parent 780c35eabe
commit 93d8e22b07
8 changed files with 149 additions and 59 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 939 B

View File

@@ -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)
);
};

View File

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

View File

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

View File

@@ -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 = () => {
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 = () => {
<SettingWrapper
title={t['com.affine.settings.meetings.privacy.header']()}
>
<SettingRow
name={t[
'com.affine.settings.meetings.privacy.screen-system-audio-recording'
]()}
desc={
<>
{t[
'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>
)}
</>
<PermissionSettingRow
nameKey="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"
hasPermission={permissions?.screen || false}
onOpenPermissionSetting={
handleOpenScreenRecordingPermissionSetting
}
>
<IconButton
icon={
screenRecordingPermission ? (
<DoneIcon />
) : (
<InformationFillDuotoneIcon
className={styles.noPermissionIcon}
/>
)
}
onClick={handleOpenScreenRecordingPermissionSetting}
/>
</SettingRow>
/>
<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
}
/>
</SettingWrapper>
</>
)}

View File

@@ -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']) => {

View File

@@ -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`
*/

View File

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