Show a modal for macOS media permissions

This commit is contained in:
Fedor Indutny 2025-02-27 11:09:06 -08:00 committed by GitHub
parent 71dea5cbf3
commit 0c875b444b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 461 additions and 14 deletions

View file

@ -6789,6 +6789,38 @@
"messageformat": "9:16",
"description": "Media editor > image editing controls > crop tool > Crop presets > 9:16 (9 by 16)"
},
"icu:MediaPermissionsModal__title--microphone": {
"messageformat": "Allow access to your microphone",
"description": "Title of MediaPermissionsModal when notifying user about absent microphone permissions"
},
"icu:MediaPermissionsModal__title--camera": {
"messageformat": "Allow access to your camera",
"description": "Title of MediaPermissionsModal when notifying user about absent camera permissions"
},
"icu:MediaPermissionsModal__subtitle--call": {
"messageformat": "To join or start a call:",
"description": "Subtitle of MediaPermissionsModal when notifying user about insufficient microphone permissions for starting a call"
},
"icu:MediaPermissionsModal__subtitle--call--camera": {
"messageformat": "To enable your video:",
"description": "Subtitle of MediaPermissionsModal when notifying user about insufficient camera permissions for starting a call"
},
"icu:MediaPermissionsModal__subtitle--voice-note": {
"messageformat": "To send voice messages:",
"description": "Subtitle of MediaPermissionsModal when notifying user about insufficient media permissions for recording a voice note"
},
"icu:MediaPermissionsModal__step-1": {
"messageformat": "Tap “{buttonName}” below",
"description": "Text of the first step of the instructions from MediaPermissionsModal"
},
"icu:MediaPermissionsModal__step-2": {
"messageformat": "Turn on “Signal”",
"description": "Text of the second step of the instructions from MediaPermissionsModal"
},
"icu:MediaPermissionsModal__open": {
"messageformat": "Go to settings",
"description": "Text of the button opening system media permissions preferences window"
},
"icu:MyStories__title": {
"messageformat": "My Stories",
"description": "Title for the my stories list"

View file

@ -2988,12 +2988,32 @@ async function ensureFilePermissions(onlyFiles?: Array<string>) {
ipc.handle('get-media-access-status', async (_event, value) => {
// This function is not supported on Linux
if (!systemPreferences.getMediaAccessStatus) {
return undefined;
return 'unknown';
}
return systemPreferences.getMediaAccessStatus(value);
});
ipc.handle(
'open-system-media-permissions',
async (_event, mediaType: 'camera' | 'microphone') => {
if (!OS.isMacOS()) {
return;
}
if (mediaType === 'camera') {
await shell.openExternal(
'x-apple.systempreferences:com.apple.preference.security?Privacy_Camera'
);
} else if (mediaType === 'microphone') {
await shell.openExternal(
'x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone'
);
} else {
throw missingCaseError(mediaType);
}
}
);
ipc.handle('get-auto-launch', async () => {
return app.getLoginItemSettings(await getDefaultLoginItemSettings())
.openAtLogin;

17
images/macos-switch.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="30" height="21" viewBox="0 0 30 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="1" width="28" height="17" rx="8.5" fill="#1D87FF"/>
<g filter="url(#filter0_d_1_1422)">
<ellipse cx="19.5152" cy="9.50001" rx="6.78788" ry="6.8" fill="white"/>
</g>
<defs>
<filter id="filter0_d_1_1422" x="9.32729" y="0.150012" width="20.3758" height="20.4" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.85"/>
<feGaussianBlur stdDeviation="1.7"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.16 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_1422"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_1422" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View file

@ -0,0 +1,49 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.MediaPermissionsModal__headerTitle {
padding-block: 10px;
}
.MediaPermissionsModal__body {
display: flex;
flex-direction: column;
align-items: center;
}
.MediaPermissionsModal h1 {
@include mixins.font-title-medium;
margin-inline: 0;
margin-block: 0 6px;
line-height: 24px;
}
.MediaPermissionsModal__subtitle {
@include mixins.font-body-1;
margin-inline: 0;
margin-block: 0 20px;
@include mixins.light-theme {
color: rgba(variables.$color-gray-60, 0.8);
}
@include mixins.dark-theme {
color: variables.$color-gray-25;
}
}
.MediaPermissionsModal ol {
display: flex;
flex-direction: column;
gap: 12px;
margin-inline: 0;
margin-block: 0 24px;
text-align: start;
}

View file

@ -125,6 +125,7 @@
@use 'components/ListTile.scss';
@use 'components/LocalDeleteWarningModal.scss';
@use 'components/MediaEditor.scss';
@use 'components/MediaPermissionsModal.scss';
@use 'components/MediaQualitySelector.scss';
@use 'components/MessageAudio.scss';
@use 'components/MessageBody.scss';

View file

@ -20,6 +20,7 @@ import { ButtonVariant } from './Button';
import { ConfirmationDialog } from './ConfirmationDialog';
import { SignalConnectionsModal } from './SignalConnectionsModal';
import { WhatsNewModal } from './WhatsNewModal';
import { MediaPermissionsModal } from './MediaPermissionsModal';
import type { StartCallData } from './ConfirmLeaveCallModal';
import type { AttachmentNotAvailableModalType } from './AttachmentNotAvailableModal';
@ -74,6 +75,15 @@ export type PropsType = {
// ForwardMessageModal
forwardMessagesProps: ForwardMessagesPropsType | undefined;
renderForwardMessagesModal: () => JSX.Element;
// MediaPermissionsModal
mediaPermissionsModalProps:
| {
mediaType: 'camera' | 'microphone';
requestor: 'call' | 'voiceNote';
}
| undefined;
closeMediaPermissionsModal: () => void;
openSystemMediaPermissions: (mediaType: 'camera' | 'microphone') => void;
// MessageRequestActionsConfirmation
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
renderMessageRequestActionsConfirmation: () => JSX.Element;
@ -156,6 +166,10 @@ export function GlobalModalContainer({
// ForwardMessageModal
forwardMessagesProps,
renderForwardMessagesModal,
// MediaPermissionsModal
mediaPermissionsModalProps,
closeMediaPermissionsModal,
openSystemMediaPermissions,
// MessageRequestActionsConfirmation
messageRequestActionsConfirmationProps,
renderMessageRequestActionsConfirmation,
@ -218,6 +232,18 @@ export function GlobalModalContainer({
return renderForwardMessagesModal();
}
// Media Permissions Modal
if (mediaPermissionsModalProps) {
return (
<MediaPermissionsModal
i18n={i18n}
{...mediaPermissionsModalProps}
openSystemMediaPermissions={openSystemMediaPermissions}
onClose={closeMediaPermissionsModal}
/>
);
}
// The Rest
if (confirmLeaveCallModalState) {

View file

@ -0,0 +1,44 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { StrictMode } from 'react';
import { action } from '@storybook/addon-actions';
import enMessages from '../../_locales/en/messages.json';
import { type ComponentMeta } from '../storybook/types';
import { setupI18n } from '../util/setupI18n';
import type { PropsType } from './MediaPermissionsModal';
import { MediaPermissionsModal } from './MediaPermissionsModal';
const i18n = setupI18n('en', enMessages);
type TemplateProps = Omit<PropsType, 'i18n' | 'children'>;
function Template(props: TemplateProps) {
return (
<StrictMode>
<MediaPermissionsModal i18n={i18n} {...props} />
</StrictMode>
);
}
export default {
title: 'Components/MediaPermissionsModal',
component: Template,
args: {
mediaType: 'camera',
requestor: 'call',
openSystemMediaPermissions: action('onOpenSystemMediaPermissions'),
onClose: action('onClose'),
},
} satisfies ComponentMeta<TemplateProps>;
export function Camera(props: TemplateProps): JSX.Element {
return <Template {...props} mediaType="camera" />;
}
export function Microphone(props: TemplateProps): JSX.Element {
return <Template {...props} mediaType="microphone" />;
}
export function VoiceNote(props: TemplateProps): JSX.Element {
return <Template {...props} requestor="voiceNote" mediaType="microphone" />;
}

View file

@ -0,0 +1,82 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback } from 'react';
import { Modal } from './Modal';
import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
import { Button } from './Button';
export type PropsType = {
i18n: LocalizerType;
mediaType: 'camera' | 'microphone';
requestor: 'call' | 'voiceNote';
openSystemMediaPermissions: (mediaType: 'camera' | 'microphone') => void;
onClose: () => void;
};
export function MediaPermissionsModal({
i18n,
mediaType,
requestor,
openSystemMediaPermissions,
onClose,
}: PropsType): JSX.Element {
let title: string;
if (mediaType === 'camera') {
title = i18n('icu:MediaPermissionsModal__title--camera');
} else if (mediaType === 'microphone') {
title = i18n('icu:MediaPermissionsModal__title--microphone');
} else {
throw missingCaseError(mediaType);
}
let subtitle: string;
if (requestor === 'call') {
if (mediaType === 'camera') {
subtitle = i18n('icu:MediaPermissionsModal__subtitle--call--camera');
} else if (mediaType === 'microphone') {
subtitle = i18n('icu:MediaPermissionsModal__subtitle--call');
} else {
throw missingCaseError(mediaType);
}
} else if (requestor === 'voiceNote') {
subtitle = i18n('icu:MediaPermissionsModal__subtitle--voice-note');
} else {
throw missingCaseError(requestor);
}
const onClick = useCallback(
() => openSystemMediaPermissions(mediaType),
[openSystemMediaPermissions, mediaType]
);
return (
<Modal
modalName="MediaPermissionsModal"
hasXButton
i18n={i18n}
onClose={onClose}
moduleClassName="MediaPermissionsModal"
>
<div className="MediaPermissionsModal__body">
<h1>{title}</h1>
<p className="MediaPermissionsModal__subtitle">{subtitle}</p>
<ol>
<li>
{i18n('icu:MediaPermissionsModal__step-1', {
buttonName: i18n('icu:MediaPermissionsModal__open'),
})}
</li>
<li>
<img alt="" src="images/macos-switch.svg" width={28} height={17} />
{i18n('icu:MediaPermissionsModal__step-2')}
</li>
</ol>
<Button onClick={onClick}>
{i18n('icu:MediaPermissionsModal__open')}
</Button>
</div>
</Modal>
);
}

View file

@ -53,6 +53,11 @@ export class RecorderClass {
return false;
}
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
'microphone',
'voiceNote'
);
this.clear();
this.#context = new AudioContext();

View file

@ -327,6 +327,27 @@ export type NotifyScreenShareStatusOptionsType = Readonly<
)
>;
async function ensureSystemPermissions({
hasLocalVideo,
hasLocalAudio,
}: {
hasLocalVideo: boolean;
hasLocalAudio: boolean;
}): Promise<void> {
if (hasLocalAudio) {
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
'microphone',
'call'
);
}
if (hasLocalVideo) {
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
'camera',
'call'
);
}
}
export class CallingClass {
readonly #videoCapturer: GumVideoCapturer;
@ -502,6 +523,7 @@ export class CallingClass {
}
log.info('CallingClass.startCallingLobby(): Starting lobby');
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
// It's important that this function comes before any calls to
// `videoCapturer.enableCapture` or `videoCapturer.enableCaptureAndSend` because of
@ -522,7 +544,7 @@ export class CallingClass {
await this.#startDeviceReselectionTimer();
const enableLocalCameraIfNecessary = hasLocalVideo
? () => this.enableLocalCamera()
? () => drop(this.enableLocalCamera())
: noop;
switch (callMode) {
@ -833,6 +855,8 @@ export class CallingClass {
const roomId = getRoomIdFromRootKey(callLinkRootKey);
log.info('startCallLinkLobby() for roomId', roomId);
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
await this.#startDeviceReselectionTimer();
const authCredentialPresentation =
@ -848,7 +872,7 @@ export class CallingClass {
groupCall.setOutgoingAudioMuted(!hasLocalAudio);
groupCall.setOutgoingVideoMuted(!hasLocalVideo);
this.enableLocalCamera();
drop(this.enableLocalCamera());
return {
callMode: CallMode.Adhoc,
@ -892,6 +916,17 @@ export class CallingClass {
return;
}
try {
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
} catch (error) {
log.error(
`${logId}: failed to ensure system permissions`,
Errors.toLogFormat(error)
);
this.stopCallingLobby();
return;
}
log.info(`${logId}: Getting call settings`);
// Check state after awaiting to debounce call button.
if (RingRTC.call && RingRTC.call.state !== CallState.Ended) {
@ -1239,6 +1274,7 @@ export class CallingClass {
);
}
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
await this.#startDeviceReselectionTimer();
const groupCall = this.connectGroupCall(conversationId, {
@ -1577,6 +1613,7 @@ export class CallingClass {
);
}
await ensureSystemPermissions({ hasLocalAudio, hasLocalVideo });
await this.#startDeviceReselectionTimer();
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
@ -1924,6 +1961,10 @@ export class CallingClass {
const haveMediaPermissions = await this.#requestPermissions(asVideoCall);
if (haveMediaPermissions) {
await ensureSystemPermissions({
hasLocalAudio: true,
hasLocalVideo: asVideoCall,
});
await this.#startDeviceReselectionTimer();
RingRTC.setVideoCapturer(callId, this.#videoCapturer);
RingRTC.setVideoRenderer(callId, this.videoRenderer);
@ -2024,13 +2065,21 @@ export class CallingClass {
}
}
setOutgoingVideo(conversationId: string, enabled: boolean): void {
async setOutgoingVideo(
conversationId: string,
enabled: boolean
): Promise<void> {
const call = getOwn(this.#callsLookup, conversationId);
if (!call) {
log.warn('Trying to set outgoing video for a non-existent call');
return;
}
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
'camera',
'call'
);
if (call instanceof Call) {
RingRTC.setOutgoingVideo(call.callId, enabled);
} else if (call instanceof GroupCall) {
@ -2083,11 +2132,13 @@ export class CallingClass {
},
})
);
this.setOutgoingVideo(conversationId, true);
drop(this.setOutgoingVideo(conversationId, true));
} else {
this.setOutgoingVideo(
conversationId,
this.#hadLocalVideoBeforePresenting ?? hasLocalVideo
drop(
this.setOutgoingVideo(
conversationId,
this.#hadLocalVideoBeforePresenting ?? hasLocalVideo
)
);
this.#hadLocalVideoBeforePresenting = undefined;
}
@ -2405,8 +2456,12 @@ export class CallingClass {
RingRTC.setAudioOutput(device.index);
}
enableLocalCamera(): void {
drop(this.#videoCapturer.enableCapture());
async enableLocalCamera(): Promise<void> {
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
'camera',
'call'
);
await this.#videoCapturer.enableCapture();
}
async enableCaptureAndSend(

View file

@ -1888,9 +1888,12 @@ function setLocalVideo(
isGroupOrAdhocCallState(activeCall) ||
(activeCall.callMode === CallMode.Direct && activeCall.callState)
) {
calling.setOutgoingVideo(activeCall.conversationId, payload.enabled);
await calling.setOutgoingVideo(
activeCall.conversationId,
payload.enabled
);
} else if (payload.enabled) {
calling.enableLocalCamera();
await calling.enableLocalCamera();
} else {
calling.disableLocalVideo();
}

View file

@ -3,6 +3,7 @@
import type { ThunkAction } from 'redux-thunk';
import type { ReadonlyDeep } from 'type-fest';
import OS from '../../util/os/osMain';
import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type {
GroupV2PendingMemberType,
@ -27,6 +28,8 @@ import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
import { useBoundActions } from '../../hooks/useBoundActions';
import { isGroupV1 } from '../../util/whatTypeOfConversation';
import { sleep } from '../../util/sleep';
import { SECOND } from '../../util/durations';
import { getGroupMigrationMembers } from '../../groups';
import {
MESSAGE_CHANGED,
@ -122,6 +125,11 @@ export type GlobalModalsStateType = ReadonlyDeep<{
messageRequestActionsConfirmationProps: MessageRequestActionsConfirmationPropsType | null;
notePreviewModalProps: NotePreviewModalPropsType | null;
usernameOnboardingState: UsernameOnboardingState;
mediaPermissionsModalProps?: {
mediaType: 'camera' | 'microphone';
requestor: 'call' | 'voiceNote';
abortController: AbortController;
};
profileEditorHasError: boolean;
profileEditorInitialEditState: ProfileEditorEditState | undefined;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
@ -185,6 +193,10 @@ const CLOSE_EDIT_HISTORY_MODAL = 'globalModals/CLOSE_EDIT_HISTORY_MODAL';
const TOGGLE_USERNAME_ONBOARDING = 'globalModals/TOGGLE_USERNAME_ONBOARDING';
const TOGGLE_CONFIRM_LEAVE_CALL_MODAL =
'globalModals/TOGGLE_CONFIRM_LEAVE_CALL_MODAL';
const CLOSE_MEDIA_PERMISSIONS_MODAL =
'globalModals/CLOSE_MEDIA_PERMISSIONS_MODAL';
const SHOW_MEDIA_PERMISSIONS_MODAL =
'globalModals/SHOW_MEDIA_PERMISSIONS_MODAL';
export type ContactModalStateType = ReadonlyDeep<{
contactId: string;
@ -361,6 +373,19 @@ export type ShowErrorModalActionType = ReadonlyDeep<{
};
}>;
type CloseMediaPermissionsModalActionType = ReadonlyDeep<{
type: typeof CLOSE_MEDIA_PERMISSIONS_MODAL;
}>;
type ShowMediaPermissionsModalActionType = ReadonlyDeep<{
type: typeof SHOW_MEDIA_PERMISSIONS_MODAL;
payload: {
mediaType: 'camera' | 'microphone';
requestor: 'call' | 'voiceNote';
abortController: AbortController;
};
}>;
type ToggleEditNicknameAndNoteModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_EDIT_NICKNAME_AND_NOTE_MODAL;
payload: EditNicknameAndNoteModalPropsType | null;
@ -393,6 +418,7 @@ type CloseEditHistoryModalActionType = ReadonlyDeep<{
export type GlobalModalsActionType = ReadonlyDeep<
| CloseEditHistoryModalActionType
| CloseErrorModalActionType
| CloseMediaPermissionsModalActionType
| CloseGV2MigrationDialogActionType
| CloseShortcutGuideModalActionType
| CloseStickerPackPreviewActionType
@ -409,6 +435,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowContactModalActionType
| ShowEditHistoryModalActionType
| ShowErrorModalActionType
| ShowMediaPermissionsModalActionType
| ToggleEditNicknameAndNoteModalActionType
| ToggleMessageRequestActionsConfirmationActionType
| ShowSendAnywayDialogActionType
@ -443,6 +470,7 @@ export const actions = {
closeGV2MigrationDialog,
closeShortcutGuideModal,
closeStickerPackPreview,
closeMediaPermissionsModal,
hideAttachmentNotAvailableModal,
hideBlockingSafetyNumberChangeDialog,
hideContactModal,
@ -454,6 +482,7 @@ export const actions = {
showContactModal,
showEditHistoryModal,
showErrorModal,
ensureSystemMediaPermissions,
toggleEditNicknameAndNoteModal,
toggleMessageRequestActionsConfirmation,
showGV2MigrationDialog,
@ -937,6 +966,63 @@ function showErrorModal({
};
}
function closeMediaPermissionsModal(): CloseMediaPermissionsModalActionType {
return {
type: CLOSE_MEDIA_PERMISSIONS_MODAL,
};
}
const MEDIA_PERMISSIONS_POLL_INTERVAL = SECOND;
export function ensureSystemMediaPermissions(
mediaType: 'camera' | 'microphone',
requestor: 'call' | 'voiceNote'
): ThunkAction<
void,
RootStateType,
unknown,
ShowMediaPermissionsModalActionType | CloseMediaPermissionsModalActionType
> {
return async dispatch => {
// Only macOS supported at the moment
if (!OS.isMacOS()) {
return;
}
const status = await window.IPC.getMediaAccessStatus(mediaType);
if (status !== 'denied') {
return;
}
const logId = `ensureSystemMediaPermissions(${mediaType}, ${requestor})`;
log.warn(`${logId}: permission denied, showing UI`);
const abortController = new AbortController();
dispatch({
type: SHOW_MEDIA_PERMISSIONS_MODAL,
payload: { mediaType, requestor, abortController },
});
const { signal } = abortController;
while (!signal.aborted) {
// eslint-disable-next-line no-await-in-loop
await sleep(MEDIA_PERMISSIONS_POLL_INTERVAL, signal);
// eslint-disable-next-line no-await-in-loop
const updatedStatus = await window.IPC.getMediaAccessStatus(mediaType);
if (signal.aborted) {
throw new Error('ensureSystemMediaPermissions: modal dismissed');
}
if (updatedStatus !== 'denied') {
break;
}
}
dispatch({ type: CLOSE_MEDIA_PERMISSIONS_MODAL });
};
}
function toggleEditNicknameAndNoteModal(
payload: EditNicknameAndNoteModalPropsType | null
): ToggleEditNicknameAndNoteModalActionType {
@ -1416,5 +1502,20 @@ export function reducer(
}
}
if (action.type === CLOSE_MEDIA_PERMISSIONS_MODAL) {
state.mediaPermissionsModalProps?.abortController.abort();
return {
...state,
mediaPermissionsModalProps: undefined,
};
}
if (action.type === SHOW_MEDIA_PERMISSIONS_MODAL) {
return {
...state,
mediaPermissionsModalProps: action.payload,
};
}
return state;
}

View file

@ -126,6 +126,7 @@ export const SmartGlobalModalContainer = memo(
editNicknameAndNoteModalProps,
errorModalProps,
forwardMessagesProps,
mediaPermissionsModalProps,
messageRequestActionsConfirmationProps,
notePreviewModalProps,
isProfileEditorVisible,
@ -142,6 +143,7 @@ export const SmartGlobalModalContainer = memo(
const {
closeErrorModal,
closeMediaPermissionsModal,
hideUserNotFoundModal,
hideWhatsNewModal,
toggleSignalConnectionsModal,
@ -214,6 +216,9 @@ export const SmartGlobalModalContainer = memo(
messageRequestActionsConfirmationProps={
messageRequestActionsConfirmationProps
}
mediaPermissionsModalProps={mediaPermissionsModalProps}
closeMediaPermissionsModal={closeMediaPermissionsModal}
openSystemMediaPermissions={window.IPC.openSystemMediaPermissions}
notePreviewModalProps={notePreviewModalProps}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron';
import type { SystemPreferences } from 'electron';
import type { AudioDevice } from '@signalapp/ringrtc';
import { noop } from 'lodash';
@ -115,7 +116,7 @@ export type IPCEventsCallbacksType = {
getConversationsWithCustomColor: (x: string) => Array<ConversationType>;
getMediaAccessStatus: (
mediaType: 'screen' | 'microphone' | 'camera'
) => Promise<string | unknown>;
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
installStickerPack: (packId: string, key: string) => Promise<void>;
isPrimary: () => boolean;
removeCustomColor: (x: string) => void;

6
ts/window.d.ts vendored
View file

@ -5,6 +5,7 @@
import type { Store } from 'redux';
import type * as Backbone from 'backbone';
import type { SystemPreferences } from 'electron';
import type PQueue from 'p-queue/dist';
import type { assert } from 'chai';
import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber';
@ -68,8 +69,11 @@ export type IPCType = {
getAutoLaunch: () => Promise<boolean>;
getMediaAccessStatus: (
mediaType: 'screen' | 'microphone' | 'camera'
) => Promise<string | undefined>;
) => Promise<ReturnType<SystemPreferences['getMediaAccessStatus']>>;
getMediaCameraPermissions: () => Promise<boolean>;
openSystemMediaPermissions: (
mediaType: 'microphone' | 'camera'
) => Promise<void>;
getMediaPermissions: () => Promise<boolean>;
logAppLoadedEvent?: (options: { processedCount?: number }) => void;
readyForUpdates: () => void;

View file

@ -88,6 +88,8 @@ const IPC: IPCType = {
getAutoLaunch: () => ipc.invoke('get-auto-launch'),
getMediaAccessStatus: mediaType =>
ipc.invoke('get-media-access-status', mediaType),
openSystemMediaPermissions: mediaType =>
ipc.invoke('open-system-media-permissions', mediaType),
getMediaPermissions: () => ipc.invoke('settings:get:mediaPermissions'),
getMediaCameraPermissions: () =>
ipc.invoke('settings:get:mediaCameraPermissions'),