From 93d8e22b074eb421a9c2459cda02ac22cae06143 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Tue, 8 Apr 2025 10:18:08 +0000 Subject: [PATCH] feat(electron): meeting recording permissions checks (#11505) fix AF-2472, AF-2446 ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/536f8412-cbf1-47ad-a8a0-0b86e97521d7.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/b49a64cc-d288-49c9-8382-e4e8b5806850.png) --- .../electron/resources/icons/tray-icon.png | Bin 2512 -> 939 bytes .../electron/src/main/recording/feature.ts | 23 +++- .../apps/electron/src/main/recording/index.ts | 14 +- .../apps/electron/src/main/tray/index.ts | 19 ++- .../general-setting/meetings/index.tsx | 127 ++++++++++++------ .../media/services/meeting-settings.ts | 10 +- packages/frontend/i18n/src/i18n.gen.ts | 12 ++ packages/frontend/i18n/src/resources/en.json | 3 + 8 files changed, 149 insertions(+), 59 deletions(-) diff --git a/packages/frontend/apps/electron/resources/icons/tray-icon.png b/packages/frontend/apps/electron/resources/icons/tray-icon.png index 4385d9b11a3dbf0bda9a91d7c219b905bc03bcdc..09ad658d2b558d1b2910de8a98a916d6f2086bd3 100644 GIT binary patch literal 939 zcmV;c162HpP)d%=i^T;-e6f1yU7U zWagDhb&wE*;?PIVU_|oI@~$Oj(NAuL{*oO81R;q#=f6RjoMwqLY%oB?9fqZ`c{I-C zECww971BMWodxe3L7rSJ{}l#;I50N#S2R};wpE^-!2pFMC+|6e;^u46w@Aq~7?Au6 z%4E#W){(D9N6;c9H%FK?!3&~BKQVoJ~2|S|Qb#N9|2n9rsuV}p%FPOCo;hk?N5DrSP587fc7<@Bwq;vlN_8CE< z0J&3NAyvcJ4rMjAljQ$Bo{npDinZR^Z+Co8#~$K>dLX-Ze~*P;|Lj>4gm+WVeU`L0 zv?!}1!)IB`Pi6j?EDD~0r)F$H$KSEP^B>*$FB*HG<1gtl|9s!jwO*RZPw1NG7&V>= z6~etUbHnpQACvvF!9Q3z9}CBfi1}BQHTGrJ&^jBpRvnRA zAXUjVQZc$Ht_zEEa0=_rt@E`A$u4OPf1N@4Tr>0=79lx{3qcTOOK8md@1h`9=tRXJ zUtYc|VOh_q-wYw~i*^4sEy^2MkUm5$%`@mo8l=xACcBD))#D2La?B~_*^6kR=sGoylYfIVz$IX#v0H?~!Nj~+G=-qH6xbm$tsAd!q3*vz zzZ$ifF-tx{cw>B!){%)jd7h&|Yk9}t_T#tpE1Upcpo+!}jooF4wV9xmWGORB#P7vJ z!f@7Cu literal 2512 zcmY*bd0bP+7QP8vNJN8#5M(h#S%i`xAU0tM38Cx}Xt2m0kR>c3O@crbjX)}D1*}j7 zq9kr81x%1Of)*4ig0jeyB_b-+0xgOn2*})U6bQH`hgQ2;=xtONvQN!87-!M#thaQaH*ez$t?S!jxD(6PuBe%;u3YoN*r* zBoHgz1RVAQg`eb%3-Te~xO?MBfwZqag@Yq!TmqlZArT1a z>FM}%JA5iPp0J%rBob_F3AVP@Aj6uM$>uXNtl7LRA4UGFLyP6ba9JEaE0v8^>N2BK z)A-IfoHEhp_HmtjR@}dt*t}0=fdvwjGlcDU8^UL8Fw{xuCGFy}V!_NxeV6S{ADI8= z`;^CtpiKThi}_gT2QOHv3*3qDdD~pz`Vk`<0H6`zMWZk>Ad`xhyi@L&){#Fn16~tafE!$L!Tv^aDN$y?iyQbY`TWzNf8CzL3Q`I5Z!b zgB~8us3RM$zu8=NxR?=K^0s?KrsMh-kITafUz3k2 zRTt&WYjy?W10_=jk&8c59hY_TK~tqFZEDB$PuN)qAbcA`z?K+C(Cj z%g0EE*LXVo3AiL*ykqX}qN!{B+sU??3KdAi2FonnEn-HGTBGKGN&-2m0iCqXyxf(T1st!w_32rv7zr7Gq9VV{Z zrPdeLf2DIGA24`^W~{!TZ2cl$tE*CtyB1Oib+Jt<_(j2W7+|ZU{#js{{^Aj2NM*vt z`Xq*Qr&w&<8=a;(Jl*_Q-pv0JiE6A&Ar$P}u|qP1?2dOb$U7XnCmdR2P<}og_hr`R zLoZ7st8tg}ZFEp>XPJ*vWHnm(adsl|M${MX(wcg^zA$rStReoJ7SpS(@;WWY5WT9_wM%tp z%k_~~CufgluG;YO>hPn8`l+Hs>o7!R2EmLfJa}h_ra)aRwZEaKju+_2N@kf{;~0b3 z>32)Rx%bWN!fK0Qj{;z?&M{Ba*6hnL39hK{d_g1Zrc)c=z;*7lqslsmuTRC^z~ntV zepD(GSJO41Hch{Ty*yx?haLRMko6lJ+daCJk6jBzl%Kd!y@1(|zj5nbblJ?Vkg5fA zzxGLH=Ub_BnASjc=pR4&=Fdi>-^{VP2Od)c*(4+LzT@9c`Esro&EwswQ1@aFg@jcC z+~=o_92|7l3j>#v6D65+REKKNlWpgZwe!r$`h!`ggH@5bbA0kL^1z9&G8na&;J2^# zQC71y@_}oi-BV#2A4OJTJ&>mmSpYDN1auYF{B*jmX!IluM$4Y`18*ZsrfB5n&(nrKUj<` zR?oe$5oIMrbcCH}C4FgJwM7fobY>=V@9Q`v-`DI8639BZBty+0u5G~% zeGQ>J+k!Tlpl#Yh9KUp|aLpbWdW^Vo2_vg=dsPx;bk#q%OZphC;~K2#L%eE1T&X0M zco@W;4Whh$QiKxZMI~iF8knOCk4hQiGX$Q|@2jvZ;t>34BK-;ar!^L4eE!IVk*F)B zr!wo>MZRA8k_pDeY@68g-iqVW@H>CcNB5f;LUPxlcT`*e_iTEvS*>hRl1LxxF|&f^ zshUhMx-^76M~QDYeQVV{;tGuL~#I+WYG@t*S*GzJ(hNn)jZ8 z2X-`^ohz1GmZUZGiCVXwMORvx8z4G2B>Tc)0dj{fHe$aUYya7~A`i*uMb(n6|gM9Cq^cMy{M3vY+tgz{aWYH zn<6?rKUh2q*YA&39e!X%3Hf&UXY3{#rJh7CN}A?{b1XM&Om@dPx_dvHXiqMDPZ!O! z(mS5Nv|qxz&vRRIv4Y++*ZW~oeJIQWGvDHE{nx!A#5kiP4L@nvdTBQ9y?AH$Z*2uQ z;NFSNdREyp?Kha!+TsLQfg^LDF{@`WZ1tVznJ~TSozw+nf7>DBI?`owB%B=`b7u*D zo-}1qn!|YCCA_Bc{JlcD8zOSKH1wZF7(1~b2RzD4=FOXogOmDZL-MbPCIpC{7llyy niz7vB(#kF4&ljFk4}4jpswVOIczGFF`A+e2r_*YvQQ7|htg0K& diff --git a/packages/frontend/apps/electron/src/main/recording/feature.ts b/packages/frontend/apps/electron/src/main/recording/feature.ts index ddb69a52a..856624b33 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 124ce8935..a057e9381 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 cb11edcc7..4fd05748f 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 9b9569f34..33528d606 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 d98f6647c..490e0cbd7 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 97e48a522..446acdd85 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 fe8bc9a39..5c5fe4aef 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",