diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4f2f8a013e..dd2489910c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/app/main.ts b/app/main.ts index 67f3d42ed2..5c805dd4c0 100644 --- a/app/main.ts +++ b/app/main.ts @@ -2988,12 +2988,32 @@ async function ensureFilePermissions(onlyFiles?: Array) { 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; diff --git a/images/macos-switch.svg b/images/macos-switch.svg new file mode 100644 index 0000000000..6393b5ff60 --- /dev/null +++ b/images/macos-switch.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/stylesheets/components/MediaPermissionsModal.scss b/stylesheets/components/MediaPermissionsModal.scss new file mode 100644 index 0000000000..abf486ea21 --- /dev/null +++ b/stylesheets/components/MediaPermissionsModal.scss @@ -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; +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index e39d93f7db..3d14145fd0 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index a6aebb72c6..8687487422 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -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 ( + + ); + } + // The Rest if (confirmLeaveCallModalState) { diff --git a/ts/components/MediaPermissionsModal.stories.tsx b/ts/components/MediaPermissionsModal.stories.tsx new file mode 100644 index 0000000000..1b92c9291d --- /dev/null +++ b/ts/components/MediaPermissionsModal.stories.tsx @@ -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; + +function Template(props: TemplateProps) { + return ( + + + + ); +} + +export default { + title: 'Components/MediaPermissionsModal', + component: Template, + args: { + mediaType: 'camera', + requestor: 'call', + openSystemMediaPermissions: action('onOpenSystemMediaPermissions'), + onClose: action('onClose'), + }, +} satisfies ComponentMeta; + +export function Camera(props: TemplateProps): JSX.Element { + return