Show a modal for macOS media permissions
This commit is contained in:
parent
71dea5cbf3
commit
0c875b444b
16 changed files with 461 additions and 14 deletions
|
@ -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"
|
||||
|
|
22
app/main.ts
22
app/main.ts
|
@ -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
17
images/macos-switch.svg
Normal 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 |
49
stylesheets/components/MediaPermissionsModal.scss
Normal file
49
stylesheets/components/MediaPermissionsModal.scss
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
44
ts/components/MediaPermissionsModal.stories.tsx
Normal file
44
ts/components/MediaPermissionsModal.stories.tsx
Normal 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" />;
|
||||
}
|
82
ts/components/MediaPermissionsModal.tsx
Normal file
82
ts/components/MediaPermissionsModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -53,6 +53,11 @@ export class RecorderClass {
|
|||
return false;
|
||||
}
|
||||
|
||||
await window.reduxActions.globalModals.ensureSystemMediaPermissions(
|
||||
'microphone',
|
||||
'voiceNote'
|
||||
);
|
||||
|
||||
this.clear();
|
||||
|
||||
this.#context = new AudioContext();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
6
ts/window.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue