Permissions popup context iso
This commit is contained in:
parent
f3715411c6
commit
7b5faa1cc1
49 changed files with 562 additions and 506 deletions
|
@ -1,51 +0,0 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global $, i18n */
|
|
||||||
|
|
||||||
$(document).on('keydown', e => {
|
|
||||||
if (e.keyCode === 27) {
|
|
||||||
window.closePermissionsPopup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const $body = $(document.body);
|
|
||||||
|
|
||||||
async function applyTheme() {
|
|
||||||
const theme = await window.Settings.themeSetting.getValue();
|
|
||||||
$body.removeClass('light-theme');
|
|
||||||
$body.removeClass('dark-theme');
|
|
||||||
$body.addClass(`${theme === 'system' ? window.systemTheme : theme}-theme`);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme();
|
|
||||||
|
|
||||||
window.SignalContext.nativeThemeListener.subscribe(() => {
|
|
||||||
applyTheme();
|
|
||||||
});
|
|
||||||
|
|
||||||
let message;
|
|
||||||
if (window.forCalling) {
|
|
||||||
if (window.forCamera) {
|
|
||||||
message = i18n('videoCallingPermissionNeeded');
|
|
||||||
} else {
|
|
||||||
message = i18n('audioCallingPermissionNeeded');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message = i18n('audioPermissionNeeded');
|
|
||||||
}
|
|
||||||
|
|
||||||
window.showConfirmationDialog({
|
|
||||||
confirmStyle: 'affirmative',
|
|
||||||
message,
|
|
||||||
okText: i18n('allowAccess'),
|
|
||||||
resolve: () => {
|
|
||||||
if (!window.forCamera) {
|
|
||||||
window.Settings.mediaPermissions.setValue(true);
|
|
||||||
} else {
|
|
||||||
window.Settings.mediaCameraPermissions.setValue(true);
|
|
||||||
}
|
|
||||||
window.closePermissionsPopup();
|
|
||||||
},
|
|
||||||
reject: window.closePermissionsPopup,
|
|
||||||
});
|
|
10
main.js
10
main.js
|
@ -1103,9 +1103,15 @@ function showPermissionsPopupWindow(forCalling, forCamera) {
|
||||||
...defaultWebPrefs,
|
...defaultWebPrefs,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
nodeIntegrationInWorker: false,
|
nodeIntegrationInWorker: false,
|
||||||
contextIsolation: false,
|
contextIsolation: true,
|
||||||
enableRemoteModule: true,
|
enableRemoteModule: true,
|
||||||
preload: path.join(__dirname, 'permissions_popup_preload.js'),
|
preload: path.join(
|
||||||
|
__dirname,
|
||||||
|
'ts',
|
||||||
|
'windows',
|
||||||
|
'permissions',
|
||||||
|
'preload.js'
|
||||||
|
),
|
||||||
nativeWindowOpen: true,
|
nativeWindowOpen: true,
|
||||||
},
|
},
|
||||||
parent: mainWindow,
|
parent: mainWindow,
|
||||||
|
|
|
@ -6,11 +6,7 @@
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'none';
|
content="default-src 'none';
|
||||||
child-src 'self';
|
|
||||||
connect-src 'self' https: wss:;
|
|
||||||
font-src 'self';
|
font-src 'self';
|
||||||
form-action 'self';
|
|
||||||
frame-src 'none';
|
|
||||||
img-src 'self' blob: data:;
|
img-src 'self' blob: data:;
|
||||||
media-src 'self' blob:;
|
media-src 'self' blob:;
|
||||||
object-src 'none';
|
object-src 'none';
|
||||||
|
@ -23,14 +19,13 @@
|
||||||
type="text/css"
|
type="text/css"
|
||||||
/>
|
/>
|
||||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||||
<style></style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="permissions-popup"></body>
|
<body>
|
||||||
<script type="text/javascript" src="js/components.js"></script>
|
<div id="app"></div>
|
||||||
<script type="text/javascript" src="ts/backboneJquery.js"></script>
|
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="application/javascript"
|
||||||
src="ts/shims/showConfirmationDialog.js"
|
src="ts/windows/applyTheme.js"
|
||||||
></script>
|
></script>
|
||||||
<script type="text/javascript" src="js/permissions_popup_start.js"></script>
|
<script type="application/javascript" src="ts/windows/init.js"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
// Copyright 2018-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global window */
|
|
||||||
|
|
||||||
window.React = require('react');
|
|
||||||
window.ReactDOM = require('react-dom');
|
|
||||||
|
|
||||||
const { ipcRenderer } = require('electron');
|
|
||||||
const url = require('url');
|
|
||||||
|
|
||||||
// It is important to call this as early as possible
|
|
||||||
require('./ts/windows/context');
|
|
||||||
|
|
||||||
const i18n = require('./js/modules/i18n');
|
|
||||||
const { ConfirmationDialog } = require('./ts/components/ConfirmationDialog');
|
|
||||||
const {
|
|
||||||
getEnvironment,
|
|
||||||
setEnvironment,
|
|
||||||
parseEnvironment,
|
|
||||||
} = require('./ts/environment');
|
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
|
||||||
const { locale } = config;
|
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
|
||||||
setEnvironment(parseEnvironment(config.environment));
|
|
||||||
|
|
||||||
const { createSetting } = require('./ts/util/preload');
|
|
||||||
|
|
||||||
window.getEnvironment = getEnvironment;
|
|
||||||
window.getVersion = () => config.version;
|
|
||||||
window.theme = config.theme;
|
|
||||||
window.i18n = i18n.setup(locale, localeMessages);
|
|
||||||
window.forCalling = config.forCalling === 'true';
|
|
||||||
window.forCamera = config.forCamera === 'true';
|
|
||||||
window.Signal = {
|
|
||||||
Components: {
|
|
||||||
ConfirmationDialog,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
require('./ts/logging/set_up_renderer_logging').initialize();
|
|
||||||
|
|
||||||
window.closePermissionsPopup = () =>
|
|
||||||
ipcRenderer.send('close-permissions-popup');
|
|
||||||
|
|
||||||
window.Backbone = require('backbone');
|
|
||||||
|
|
||||||
window.Settings = {
|
|
||||||
mediaCameraPermissions: createSetting('mediaCameraPermissions', {
|
|
||||||
getter: false,
|
|
||||||
}),
|
|
||||||
mediaPermissions: createSetting('mediaPermissions', {
|
|
||||||
getter: false,
|
|
||||||
}),
|
|
||||||
themeSetting: createSetting('themeSetting', { setter: false }),
|
|
||||||
};
|
|
35
stylesheets/components/PermissionsPopup.scss
Normal file
35
stylesheets/components/PermissionsPopup.scss
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.PermissionsPopup {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
@include light-theme() {
|
||||||
|
background: $color-white;
|
||||||
|
color: $color-gray-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme() {
|
||||||
|
background: $color-gray-95;
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
@include font-body-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,7 @@
|
||||||
@import './components/MessageAudio.scss';
|
@import './components/MessageAudio.scss';
|
||||||
@import './components/MessageDetail.scss';
|
@import './components/MessageDetail.scss';
|
||||||
@import './components/Modal.scss';
|
@import './components/Modal.scss';
|
||||||
|
@import './components/PermissionsPopup.scss';
|
||||||
@import './components/Preferences.scss';
|
@import './components/Preferences.scss';
|
||||||
@import './components/ProfileEditor.scss';
|
@import './components/ProfileEditor.scss';
|
||||||
@import './components/ReactionPickerPicker.scss';
|
@import './components/ReactionPickerPicker.scss';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
closeAbout: () => unknown;
|
closeAbout: () => unknown;
|
||||||
|
@ -17,21 +18,7 @@ export const About = ({
|
||||||
environment,
|
environment,
|
||||||
version,
|
version,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
useEffect(() => {
|
useEscapeHandling(closeAbout);
|
||||||
const handler = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
closeAbout();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handler);
|
|
||||||
};
|
|
||||||
}, [closeAbout]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="About">
|
<div className="About">
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Inbox } from './Inbox';
|
||||||
import { Install } from './Install';
|
import { Install } from './Install';
|
||||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
import { usePageVisibility } from '../util/hooks';
|
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
appView: AppViewType;
|
appView: AppViewType;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { Avatar, Props as AvatarProps } from './Avatar';
|
import { Avatar, Props as AvatarProps } from './Avatar';
|
||||||
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||||
|
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
import { AvatarColors } from '../types/Colors';
|
import { AvatarColors } from '../types/Colors';
|
||||||
import { SetRendererCanvasType } from '../state/ducks/calling';
|
import { SetRendererCanvasType } from '../state/ducks/calling';
|
||||||
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||||
import { usePageVisibility } from '../util/hooks';
|
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import React, {
|
||||||
ReactNode,
|
ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { usePrevious } from '../util/hooks';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { scrollToBottom } from '../util/scrollToBottom';
|
import { scrollToBottom } from '../util/scrollToBottom';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { Intl } from './Intl';
|
import { Intl } from './Intl';
|
||||||
import { ContactName } from './conversation/ContactName';
|
import { ContactName } from './conversation/ContactName';
|
||||||
import { useIntersectionObserver } from '../util/hooks';
|
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
|
||||||
import { MAX_FRAME_SIZE } from '../calling/constants';
|
import { MAX_FRAME_SIZE } from '../calling/constants';
|
||||||
|
|
||||||
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;
|
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
} from '../types/Calling';
|
} from '../types/Calling';
|
||||||
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { usePageVisibility } from '../util/hooks';
|
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||||
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
|
|
||||||
import * as OS from '../OS';
|
import * as OS from '../OS';
|
||||||
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
import { LocalizerType, ScrollBehavior } from '../types/Util';
|
||||||
import { usePrevious } from '../util/hooks';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
|
||||||
import { ConversationList } from './ConversationList';
|
import { ConversationList } from './ConversationList';
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
|
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
|
||||||
import { formatDuration } from '../util/formatDuration';
|
import { formatDuration } from '../util/formatDuration';
|
||||||
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { LocalizerType } from '../types/Util';
|
||||||
import { ModalHost } from './ModalHost';
|
import { ModalHost } from './ModalHost';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { getClassNamesFor } from '../util/getClassNamesFor';
|
import { getClassNamesFor } from '../util/getClassNamesFor';
|
||||||
import { useHasWrapped } from '../util/hooks';
|
import { useHasWrapped } from '../hooks/useHasWrapped';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import React, { useEffect } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { Theme, themeClassName } from '../util/theme';
|
import { Theme, themeClassName } from '../util/theme';
|
||||||
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
readonly noMouseClose?: boolean;
|
readonly noMouseClose?: boolean;
|
||||||
|
@ -30,25 +31,7 @@ export const ModalHost = React.memo(
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEscapeHandling(onEscape || onClose);
|
||||||
const handler = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
if (onEscape) {
|
|
||||||
onEscape();
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handler);
|
|
||||||
};
|
|
||||||
}, [onEscape, onClose]);
|
|
||||||
|
|
||||||
// This makes it easier to write dialogs to be hosted here; they won't have to worry
|
// This makes it easier to write dialogs to be hosted here; they won't have to worry
|
||||||
// as much about preventing propagation of mouse events.
|
// as much about preventing propagation of mouse events.
|
||||||
|
|
51
ts/components/PermissionsPopup.tsx
Normal file
51
ts/components/PermissionsPopup.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
message: string;
|
||||||
|
onAccept: () => unknown;
|
||||||
|
onClose: () => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function focusRef(el: HTMLElement | null) {
|
||||||
|
if (el) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionsPopup = ({
|
||||||
|
i18n,
|
||||||
|
message,
|
||||||
|
onAccept,
|
||||||
|
onClose,
|
||||||
|
}: PropsType): JSX.Element => {
|
||||||
|
useEscapeHandling(onClose);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="PermissionsPopup">
|
||||||
|
<div className="PermissionsPopup__body">{message}</div>
|
||||||
|
<div className="PermissionsPopup__buttons">
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
ref={focusRef}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
>
|
||||||
|
{i18n('confirmation-dialog--Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onAccept}
|
||||||
|
ref={focusRef}
|
||||||
|
variant={ButtonVariant.Primary}
|
||||||
|
>
|
||||||
|
{i18n('allowAccess')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -33,6 +33,7 @@ import {
|
||||||
DEFAULT_DURATIONS_SET,
|
DEFAULT_DURATIONS_SET,
|
||||||
format as formatExpirationTimer,
|
format as formatExpirationTimer,
|
||||||
} from '../util/expirationTimer';
|
} from '../util/expirationTimer';
|
||||||
|
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||||
|
|
||||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||||
|
@ -280,21 +281,7 @@ export const Preferences = ({
|
||||||
doneRendering();
|
doneRendering();
|
||||||
}, [doneRendering]);
|
}, [doneRendering]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEscapeHandling(closeSettings);
|
||||||
const handler = (event: KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
closeSettings();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handler);
|
|
||||||
};
|
|
||||||
}, [closeSettings]);
|
|
||||||
|
|
||||||
const onZoomSelectChange = useCallback(
|
const onZoomSelectChange = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
getCallingIcon,
|
getCallingIcon,
|
||||||
getCallingNotificationText,
|
getCallingNotificationText,
|
||||||
} from '../../util/callingNotification';
|
} from '../../util/callingNotification';
|
||||||
import { usePrevious } from '../../util/hooks';
|
import { usePrevious } from '../../hooks/usePrevious';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||||
import type { TimelineItemType } from './TimelineItem';
|
import type { TimelineItemType } from './TimelineItem';
|
||||||
|
|
|
@ -6,7 +6,7 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
|
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Modal } from '../Modal';
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { Emojify } from './Emojify';
|
import { Emojify } from './Emojify';
|
||||||
|
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
|
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { convertShortName } from '../emoji/lib';
|
import { convertShortName } from '../emoji/lib';
|
||||||
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { canCustomizePreferredReactions } from '../../util/canCustomizePreferredReactions';
|
import { canCustomizePreferredReactions } from '../../util/canCustomizePreferredReactions';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -7,9 +7,10 @@ import classNames from 'classnames';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
import { Avatar, Props as AvatarProps } from '../Avatar';
|
import { Avatar, Props as AvatarProps } from '../Avatar';
|
||||||
import { Emoji } from '../emoji/Emoji';
|
import { Emoji } from '../emoji/Emoji';
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import { ConversationType } from '../../state/ducks/conversations';
|
||||||
import { emojiToData, EmojiData } from '../emoji/lib';
|
import { emojiToData, EmojiData } from '../emoji/lib';
|
||||||
|
import { useEscapeHandling } from '../../hooks/useEscapeHandling';
|
||||||
|
|
||||||
export type Reaction = {
|
export type Reaction = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
@ -124,20 +125,9 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
|
||||||
selectedReactionCategory,
|
selectedReactionCategory,
|
||||||
setSelectedReactionCategory,
|
setSelectedReactionCategory,
|
||||||
] = React.useState(pickedReaction || 'all');
|
] = React.useState(pickedReaction || 'all');
|
||||||
|
|
||||||
// Handle escape key
|
// Handle escape key
|
||||||
React.useEffect(() => {
|
useEscapeHandling(onClose);
|
||||||
const handler = (e: KeyboardEvent) => {
|
|
||||||
if (onClose && e.key === 'Escape') {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handler);
|
|
||||||
};
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
// Focus first button and restore focus on unmount
|
// Focus first button and restore focus on unmount
|
||||||
const [focusRef] = useRestoreFocus();
|
const [focusRef] = useRestoreFocus();
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { StickerPackType } from '../../state/ducks/stickers';
|
import { StickerPackType } from '../../state/ducks/stickers';
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
readonly onClose: () => unknown;
|
readonly onClose: () => unknown;
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
/* eslint-disable no-restricted-syntax */
|
|
||||||
|
|
||||||
import { NativeThemeState } from '../types/NativeThemeNotifier.d';
|
|
||||||
|
|
||||||
export type Callback = (change: NativeThemeState) => void;
|
|
||||||
|
|
||||||
export interface MinimalIPC {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
sendSync(channel: string): any;
|
|
||||||
|
|
||||||
on(
|
|
||||||
channel: string,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
listener: (event: unknown, ...args: ReadonlyArray<any>) => void
|
|
||||||
): this;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SystemThemeHolder = { systemTheme: 'dark' | 'light' };
|
|
||||||
|
|
||||||
export class NativeThemeListener {
|
|
||||||
private readonly subscribers = new Array<Callback>();
|
|
||||||
|
|
||||||
public theme: NativeThemeState;
|
|
||||||
|
|
||||||
constructor(ipc: MinimalIPC, private readonly holder: SystemThemeHolder) {
|
|
||||||
this.theme = ipc.sendSync('native-theme:init');
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
ipc.on(
|
|
||||||
'native-theme:changed',
|
|
||||||
(_event: unknown, change: NativeThemeState) => {
|
|
||||||
this.theme = change;
|
|
||||||
this.update();
|
|
||||||
|
|
||||||
for (const fn of this.subscribers) {
|
|
||||||
fn(change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public subscribe(fn: Callback): void {
|
|
||||||
this.subscribers.push(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private update(): void {
|
|
||||||
this.holder.systemTheme = this.theme.shouldUseDarkColors ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
}
|
|
69
ts/context/createNativeThemeListener.ts
Normal file
69
ts/context/createNativeThemeListener.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
|
||||||
|
import { NativeThemeState } from '../types/NativeThemeNotifier.d';
|
||||||
|
|
||||||
|
export type Callback = (change: NativeThemeState) => void;
|
||||||
|
|
||||||
|
export interface MinimalIPC {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
sendSync(channel: string): any;
|
||||||
|
|
||||||
|
on(
|
||||||
|
channel: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
listener: (event: unknown, ...args: ReadonlyArray<any>) => void
|
||||||
|
): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemThemeType = 'dark' | 'light';
|
||||||
|
|
||||||
|
export type SystemThemeHolder = { systemTheme: SystemThemeType };
|
||||||
|
|
||||||
|
type NativeThemeType = {
|
||||||
|
getSystemTheme: () => SystemThemeType;
|
||||||
|
subscribe: (fn: Callback) => void;
|
||||||
|
update: () => SystemThemeType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createNativeThemeListener(
|
||||||
|
ipc: MinimalIPC,
|
||||||
|
holder: SystemThemeHolder
|
||||||
|
): NativeThemeType {
|
||||||
|
const subscribers = new Array<Callback>();
|
||||||
|
|
||||||
|
let theme = ipc.sendSync('native-theme:init');
|
||||||
|
let systemTheme: SystemThemeType;
|
||||||
|
|
||||||
|
function update(): SystemThemeType {
|
||||||
|
const nextSystemTheme = theme.shouldUseDarkColors ? 'dark' : 'light';
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
holder.systemTheme = nextSystemTheme;
|
||||||
|
return nextSystemTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(fn: Callback): void {
|
||||||
|
subscribers.push(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc.on(
|
||||||
|
'native-theme:changed',
|
||||||
|
(_event: unknown, change: NativeThemeState) => {
|
||||||
|
theme = change;
|
||||||
|
systemTheme = update();
|
||||||
|
|
||||||
|
for (const fn of subscribers) {
|
||||||
|
fn(change);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
systemTheme = update();
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSystemTheme: () => systemTheme,
|
||||||
|
subscribe,
|
||||||
|
update,
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,7 +2,10 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { Bytes } from './Bytes';
|
import { Bytes } from './Bytes';
|
||||||
import { NativeThemeListener, MinimalIPC } from './NativeThemeListener';
|
import {
|
||||||
|
createNativeThemeListener,
|
||||||
|
MinimalIPC,
|
||||||
|
} from './createNativeThemeListener';
|
||||||
|
|
||||||
export class Context {
|
export class Context {
|
||||||
public readonly bytes = new Bytes();
|
public readonly bytes = new Bytes();
|
||||||
|
@ -10,6 +13,6 @@ export class Context {
|
||||||
public readonly nativeThemeListener;
|
public readonly nativeThemeListener;
|
||||||
|
|
||||||
constructor(ipc: MinimalIPC) {
|
constructor(ipc: MinimalIPC) {
|
||||||
this.nativeThemeListener = new NativeThemeListener(ipc, window);
|
this.nativeThemeListener = createNativeThemeListener(ipc, window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { usePrevious } from '../util/hooks';
|
import { usePrevious } from './usePrevious';
|
||||||
|
|
||||||
type RemoteParticipant = {
|
type RemoteParticipant = {
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
|
16
ts/hooks/useBoundActions.ts
Normal file
16
ts/hooks/useBoundActions.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ActionCreatorsMapObject, bindActionCreators } from 'redux';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useBoundActions = <T extends ActionCreatorsMapObject>(
|
||||||
|
actions: T
|
||||||
|
): T => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return bindActionCreators(actions, dispatch);
|
||||||
|
}, [actions, dispatch]);
|
||||||
|
};
|
26
ts/hooks/useEscapeHandling.ts
Normal file
26
ts/hooks/useEscapeHandling.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useEscapeHandling(handleEscape?: () => unknown): void {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!handleEscape) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
handleEscape();
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handler);
|
||||||
|
};
|
||||||
|
}, [handleEscape]);
|
||||||
|
}
|
56
ts/hooks/useHasWrapped.ts
Normal file
56
ts/hooks/useHasWrapped.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { Ref, useEffect, useState } from 'react';
|
||||||
|
import { first, last, noop } from 'lodash';
|
||||||
|
|
||||||
|
function getTop(element: Readonly<Element>): number {
|
||||||
|
return element.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWrapped(element: Readonly<null | HTMLElement>): boolean {
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { children } = element;
|
||||||
|
const firstChild = first(children);
|
||||||
|
const lastChild = last(children);
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
firstChild &&
|
||||||
|
lastChild &&
|
||||||
|
firstChild !== lastChild &&
|
||||||
|
getTop(firstChild) !== getTop(lastChild)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a ref (to put on your element) and a boolean. The boolean will be
|
||||||
|
* `true` if the element's children have different `top`s, and `false` otherwise.
|
||||||
|
*/
|
||||||
|
export function useHasWrapped<T extends HTMLElement>(): [Ref<T>, boolean] {
|
||||||
|
const [element, setElement] = useState<null | T>(null);
|
||||||
|
|
||||||
|
const [hasWrapped, setHasWrapped] = useState(isWrapped(element));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!element) {
|
||||||
|
return noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can remove this `any` when we upgrade to TypeScript 4.2+, which adds
|
||||||
|
// `ResizeObserver` type definitions.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const observer = new (window as any).ResizeObserver(() => {
|
||||||
|
setHasWrapped(isWrapped(element));
|
||||||
|
});
|
||||||
|
observer.observe(element);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [element]);
|
||||||
|
|
||||||
|
return [setElement, hasWrapped];
|
||||||
|
}
|
64
ts/hooks/useIntersectionObserver.ts
Normal file
64
ts/hooks/useIntersectionObserver.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A light hook wrapper around `IntersectionObserver`.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* function MyComponent() {
|
||||||
|
* const [intersectionRef, intersectionEntry] = useIntersectionObserver();
|
||||||
|
* const isVisible = intersectionEntry
|
||||||
|
* ? intersectionEntry.isIntersecting
|
||||||
|
* : true;
|
||||||
|
*
|
||||||
|
* return (
|
||||||
|
* <div ref={intersectionRef}>
|
||||||
|
* I am {isVisible ? 'on the screen' : 'invisible'}
|
||||||
|
* </div>
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function useIntersectionObserver(): [
|
||||||
|
(el?: Element | null) => void,
|
||||||
|
IntersectionObserverEntry | null
|
||||||
|
] {
|
||||||
|
const [
|
||||||
|
intersectionObserverEntry,
|
||||||
|
setIntersectionObserverEntry,
|
||||||
|
] = useState<IntersectionObserverEntry | null>(null);
|
||||||
|
|
||||||
|
const unobserveRef = useRef<(() => unknown) | null>(null);
|
||||||
|
|
||||||
|
const setRef = useCallback((el?: Element | null) => {
|
||||||
|
if (unobserveRef.current) {
|
||||||
|
unobserveRef.current();
|
||||||
|
unobserveRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
if (entries.length !== 1) {
|
||||||
|
log.error(
|
||||||
|
'IntersectionObserverWrapper was observing the wrong number of elements'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries.forEach(entry => {
|
||||||
|
setIntersectionObserverEntry(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unobserveRef.current = observer.unobserve.bind(observer, el);
|
||||||
|
|
||||||
|
observer.observe(el);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [setRef, intersectionObserverEntry];
|
||||||
|
}
|
26
ts/hooks/usePageVisibility.ts
Normal file
26
ts/hooks/usePageVisibility.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function usePageVisibility(): boolean {
|
||||||
|
const [result, setResult] = useState(!document.hidden);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onVisibilityChange = () => {
|
||||||
|
setResult(!document.hidden);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener(
|
||||||
|
'visibilitychange',
|
||||||
|
onVisibilityChange,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
11
ts/hooks/usePrevious.ts
Normal file
11
ts/hooks/usePrevious.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export function usePrevious<T>(initialValue: T, currentValue: T): T {
|
||||||
|
const previousValueRef = useRef<T>(initialValue);
|
||||||
|
const result = previousValueRef.current;
|
||||||
|
previousValueRef.current = currentValue;
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2021 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { useBoundActions } from '../../util/hooks';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SwitchToAssociatedViewActionType,
|
SwitchToAssociatedViewActionType,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { take, uniq } from 'lodash';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { useBoundActions } from '../../util/hooks';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
|
|
||||||
const { updateEmojiUsage } = dataInterface;
|
const { updateEmojiUsage } = dataInterface;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { v4 as getGuid } from 'uuid';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { StateType as RootStateType } from '../reducer';
|
import { StateType as RootStateType } from '../reducer';
|
||||||
import * as storageShim from '../../shims/storage';
|
import * as storageShim from '../../shims/storage';
|
||||||
import { useBoundActions } from '../../util/hooks';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import {
|
import {
|
||||||
ConversationColors,
|
ConversationColors,
|
||||||
ConversationColorType,
|
ConversationColorType,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { omit } from 'lodash';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
import * as Errors from '../../types/errors';
|
import * as Errors from '../../types/errors';
|
||||||
import { replaceIndex } from '../../util/replaceIndex';
|
import { replaceIndex } from '../../util/replaceIndex';
|
||||||
import { useBoundActions } from '../../util/hooks';
|
import { useBoundActions } from '../../hooks/useBoundActions';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants';
|
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants';
|
||||||
import { getPreferredReactionEmoji } from '../../reactions/preferredReactionEmoji';
|
import { getPreferredReactionEmoji } from '../../reactions/preferredReactionEmoji';
|
||||||
|
|
|
@ -5,10 +5,10 @@ import { assert } from 'chai';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NativeThemeListener,
|
createNativeThemeListener,
|
||||||
MinimalIPC,
|
MinimalIPC,
|
||||||
SystemThemeHolder,
|
SystemThemeHolder,
|
||||||
} from '../../context/NativeThemeListener';
|
} from '../../context/createNativeThemeListener';
|
||||||
import { NativeThemeState } from '../../types/NativeThemeNotifier.d';
|
import { NativeThemeState } from '../../types/NativeThemeNotifier.d';
|
||||||
|
|
||||||
class FakeIPC extends EventEmitter implements MinimalIPC {
|
class FakeIPC extends EventEmitter implements MinimalIPC {
|
||||||
|
@ -26,7 +26,7 @@ describe('NativeThemeListener', () => {
|
||||||
const holder: SystemThemeHolder = { systemTheme: 'dark' };
|
const holder: SystemThemeHolder = { systemTheme: 'dark' };
|
||||||
|
|
||||||
it('syncs the initial native theme', () => {
|
it('syncs the initial native theme', () => {
|
||||||
const dark = new NativeThemeListener(
|
const dark = createNativeThemeListener(
|
||||||
new FakeIPC({
|
new FakeIPC({
|
||||||
shouldUseDarkColors: true,
|
shouldUseDarkColors: true,
|
||||||
}),
|
}),
|
||||||
|
@ -34,9 +34,9 @@ describe('NativeThemeListener', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(holder.systemTheme, 'dark');
|
assert.strictEqual(holder.systemTheme, 'dark');
|
||||||
assert.isTrue(dark.theme.shouldUseDarkColors);
|
assert.strictEqual(dark.getSystemTheme(), 'dark');
|
||||||
|
|
||||||
const light = new NativeThemeListener(
|
const light = createNativeThemeListener(
|
||||||
new FakeIPC({
|
new FakeIPC({
|
||||||
shouldUseDarkColors: false,
|
shouldUseDarkColors: false,
|
||||||
}),
|
}),
|
||||||
|
@ -44,7 +44,7 @@ describe('NativeThemeListener', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(holder.systemTheme, 'light');
|
assert.strictEqual(holder.systemTheme, 'light');
|
||||||
assert.isFalse(light.theme.shouldUseDarkColors);
|
assert.strictEqual(light.getSystemTheme(), 'light');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should react to native theme changes', () => {
|
it('should react to native theme changes', () => {
|
||||||
|
@ -52,14 +52,14 @@ describe('NativeThemeListener', () => {
|
||||||
shouldUseDarkColors: true,
|
shouldUseDarkColors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const listener = new NativeThemeListener(ipc, holder);
|
const listener = createNativeThemeListener(ipc, holder);
|
||||||
|
|
||||||
ipc.emit('native-theme:changed', null, <NativeThemeState>{
|
ipc.emit('native-theme:changed', null, <NativeThemeState>{
|
||||||
shouldUseDarkColors: false,
|
shouldUseDarkColors: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(holder.systemTheme, 'light');
|
assert.strictEqual(holder.systemTheme, 'light');
|
||||||
assert.isFalse(listener.theme.shouldUseDarkColors);
|
assert.strictEqual(listener.getSystemTheme(), 'light');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should notify subscribers of native theme changes', done => {
|
it('should notify subscribers of native theme changes', done => {
|
||||||
|
@ -67,7 +67,7 @@ describe('NativeThemeListener', () => {
|
||||||
shouldUseDarkColors: true,
|
shouldUseDarkColors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const listener = new NativeThemeListener(ipc, holder);
|
const listener = createNativeThemeListener(ipc, holder);
|
||||||
|
|
||||||
listener.subscribe(state => {
|
listener.subscribe(state => {
|
||||||
assert.isFalse(state.shouldUseDarkColors);
|
assert.isFalse(state.shouldUseDarkColors);
|
|
@ -1,160 +0,0 @@
|
||||||
// Copyright 2020-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import { ActionCreatorsMapObject, bindActionCreators } from 'redux';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { first, last, noop } from 'lodash';
|
|
||||||
import * as log from '../../logging/log';
|
|
||||||
|
|
||||||
export function usePrevious<T>(initialValue: T, currentValue: T): T {
|
|
||||||
const previousValueRef = React.useRef<T>(initialValue);
|
|
||||||
const result = previousValueRef.current;
|
|
||||||
previousValueRef.current = currentValue;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBoundActions = <T extends ActionCreatorsMapObject>(
|
|
||||||
actions: T
|
|
||||||
): T => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
return React.useMemo(() => {
|
|
||||||
return bindActionCreators(actions, dispatch);
|
|
||||||
}, [actions, dispatch]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const usePageVisibility = (): boolean => {
|
|
||||||
const [result, setResult] = React.useState(!document.hidden);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const onVisibilityChange = () => {
|
|
||||||
setResult(!document.hidden);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVisibilityChange, false);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener(
|
|
||||||
'visibilitychange',
|
|
||||||
onVisibilityChange,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A light hook wrapper around `IntersectionObserver`.
|
|
||||||
*
|
|
||||||
* Example usage:
|
|
||||||
*
|
|
||||||
* function MyComponent() {
|
|
||||||
* const [intersectionRef, intersectionEntry] = useIntersectionObserver();
|
|
||||||
* const isVisible = intersectionEntry
|
|
||||||
* ? intersectionEntry.isIntersecting
|
|
||||||
* : true;
|
|
||||||
*
|
|
||||||
* return (
|
|
||||||
* <div ref={intersectionRef}>
|
|
||||||
* I am {isVisible ? 'on the screen' : 'invisible'}
|
|
||||||
* </div>
|
|
||||||
* );
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export function useIntersectionObserver(): [
|
|
||||||
(el?: Element | null) => void,
|
|
||||||
IntersectionObserverEntry | null
|
|
||||||
] {
|
|
||||||
const [
|
|
||||||
intersectionObserverEntry,
|
|
||||||
setIntersectionObserverEntry,
|
|
||||||
] = React.useState<IntersectionObserverEntry | null>(null);
|
|
||||||
|
|
||||||
const unobserveRef = React.useRef<(() => unknown) | null>(null);
|
|
||||||
|
|
||||||
const setRef = React.useCallback((el?: Element | null) => {
|
|
||||||
if (unobserveRef.current) {
|
|
||||||
unobserveRef.current();
|
|
||||||
unobserveRef.current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(entries => {
|
|
||||||
if (entries.length !== 1) {
|
|
||||||
log.error(
|
|
||||||
'IntersectionObserverWrapper was observing the wrong number of elements'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
entries.forEach(entry => {
|
|
||||||
setIntersectionObserverEntry(entry);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
unobserveRef.current = observer.unobserve.bind(observer, el);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return [setRef, intersectionObserverEntry];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTop(element: Readonly<Element>): number {
|
|
||||||
return element.getBoundingClientRect().top;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWrapped(element: Readonly<null | HTMLElement>): boolean {
|
|
||||||
if (!element) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { children } = element;
|
|
||||||
const firstChild = first(children);
|
|
||||||
const lastChild = last(children);
|
|
||||||
|
|
||||||
return Boolean(
|
|
||||||
firstChild &&
|
|
||||||
lastChild &&
|
|
||||||
firstChild !== lastChild &&
|
|
||||||
getTop(firstChild) !== getTop(lastChild)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook that returns a ref (to put on your element) and a boolean. The boolean will be
|
|
||||||
* `true` if the element's children have different `top`s, and `false` otherwise.
|
|
||||||
*/
|
|
||||||
export function useHasWrapped<T extends HTMLElement>(): [
|
|
||||||
React.Ref<T>,
|
|
||||||
boolean
|
|
||||||
] {
|
|
||||||
const [element, setElement] = React.useState<null | T>(null);
|
|
||||||
|
|
||||||
const [hasWrapped, setHasWrapped] = React.useState(isWrapped(element));
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!element) {
|
|
||||||
return noop;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can remove this `any` when we upgrade to TypeScript 4.2+, which adds
|
|
||||||
// `ResizeObserver` type definitions.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const observer = new (window as any).ResizeObserver(() => {
|
|
||||||
setHasWrapped(isWrapped(element));
|
|
||||||
});
|
|
||||||
observer.observe(element);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [element]);
|
|
||||||
|
|
||||||
return [setElement, hasWrapped];
|
|
||||||
}
|
|
|
@ -144,22 +144,6 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-07-21T18:34:59.251Z"
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/permissions_popup_start.js",
|
|
||||||
"line": "$(document).on('keydown', e => {",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/permissions_popup_start.js",
|
|
||||||
"line": "const $body = $(document.body);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/key_verification_view.js",
|
"path": "js/views/key_verification_view.js",
|
||||||
|
@ -12955,6 +12939,49 @@
|
||||||
"updated": "2019-11-21T06:13:49.384Z",
|
"updated": "2019-11-21T06:13:49.384Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useIntersectionObserver.ts",
|
||||||
|
"line": " const unobserveRef = useRef<(() => unknown) | null>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-09-17T20:16:37.959Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/usePrevious.ts",
|
||||||
|
"line": " const previousValueRef = useRef<T>(initialValue);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-09-17T20:16:37.959Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useRestoreFocus.js",
|
||||||
|
"line": " const lastFocusedRef = React.useRef(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-07-30T01:08:01.309Z",
|
||||||
|
"reasonDetail": "Used to store the previous-focused item, again to set focus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useRestoreFocus.js",
|
||||||
|
"line": " const toFocusRef = React.useRef(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-09-17T17:37:46.279Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useRestoreFocus.ts",
|
||||||
|
"line": " const toFocusRef = React.useRef<HTMLElement | null>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-07-30T16:57:33.618Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/hooks/useRestoreFocus.ts",
|
||||||
|
"line": " const lastFocusedRef = React.useRef<HTMLElement | null>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-07-30T16:57:33.618Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "ts/logging/debuglogs.js",
|
"path": "ts/logging/debuglogs.js",
|
||||||
|
@ -13206,65 +13233,6 @@
|
||||||
"updated": "2021-08-18T18:22:55.307Z",
|
"updated": "2021-08-18T18:22:55.307Z",
|
||||||
"reasonDetail": "Legacy code"
|
"reasonDetail": "Legacy code"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/index.js",
|
|
||||||
"line": " const unobserveRef = React.useRef(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-01-08T15:46:32.143Z",
|
|
||||||
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/index.js",
|
|
||||||
"line": " const previousValueRef = React.useRef(initialValue);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-03-18T21:41:28.361Z",
|
|
||||||
"reasonDetail": "A generic hook. Typically not to be used with non-DOM values."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/index.ts",
|
|
||||||
"line": " const previousValueRef = React.useRef<T>(initialValue);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/index.ts",
|
|
||||||
"line": " const unobserveRef = React.useRef<(() => unknown) | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/useRestoreFocus.js",
|
|
||||||
"line": " const lastFocusedRef = React.useRef(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-07-30T01:08:01.309Z",
|
|
||||||
"reasonDetail": "Used to store the previous-focused item, again to set focus"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/useRestoreFocus.js",
|
|
||||||
"line": " const toFocusRef = React.useRef(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-09-17T17:37:46.279Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/useRestoreFocus.ts",
|
|
||||||
"line": " const toFocusRef = React.useRef<HTMLElement | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/util/hooks/useRestoreFocus.ts",
|
|
||||||
"line": " const lastFocusedRef = React.useRef<HTMLElement | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2021-07-30T16:57:33.618Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "ts/views/debug_log_view.js",
|
"path": "ts/views/debug_log_view.js",
|
||||||
|
|
7
ts/window.d.ts
vendored
7
ts/window.d.ts
vendored
|
@ -116,10 +116,11 @@ import { UUID } from './types/UUID';
|
||||||
import { Address } from './types/Address';
|
import { Address } from './types/Address';
|
||||||
import { QualifiedAddress } from './types/QualifiedAddress';
|
import { QualifiedAddress } from './types/QualifiedAddress';
|
||||||
import { CI } from './CI';
|
import { CI } from './CI';
|
||||||
import { IPCEventsType } from './util/createIPCEvents';
|
import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents';
|
||||||
import { ConversationView } from './views/conversation_view';
|
import { ConversationView } from './views/conversation_view';
|
||||||
import { DebugLogView } from './views/debug_log_view';
|
import { DebugLogView } from './views/debug_log_view';
|
||||||
import { LoggerType } from './types/Logging';
|
import { LoggerType } from './types/Logging';
|
||||||
|
import { SettingType } from './util/preload';
|
||||||
|
|
||||||
export { Long } from 'long';
|
export { Long } from 'long';
|
||||||
|
|
||||||
|
@ -496,7 +497,11 @@ declare global {
|
||||||
|
|
||||||
// Context Isolation
|
// Context Isolation
|
||||||
SignalWindow: {
|
SignalWindow: {
|
||||||
|
Settings: {
|
||||||
|
themeSetting: SettingType<IPCEventsValuesType['themeSetting']>;
|
||||||
|
};
|
||||||
config: string;
|
config: string;
|
||||||
|
context: SignalContext;
|
||||||
getAppInstance: () => string | undefined;
|
getAppInstance: () => string | undefined;
|
||||||
getEnvironment: () => string;
|
getEnvironment: () => string;
|
||||||
getVersion: () => string;
|
getVersion: () => string;
|
||||||
|
|
21
ts/windows/applyTheme.ts
Normal file
21
ts/windows/applyTheme.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
async function applyTheme() {
|
||||||
|
const theme = await window.SignalWindow.Settings.themeSetting.getValue();
|
||||||
|
document.body.classList.remove('light-theme');
|
||||||
|
document.body.classList.remove('dark-theme');
|
||||||
|
document.body.classList.add(
|
||||||
|
`${
|
||||||
|
theme === 'system'
|
||||||
|
? window.SignalWindow.context.nativeThemeListener.getSystemTheme()
|
||||||
|
: theme
|
||||||
|
}-theme`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme();
|
||||||
|
|
||||||
|
window.SignalWindow.context.nativeThemeListener.subscribe(() => {
|
||||||
|
applyTheme();
|
||||||
|
});
|
|
@ -11,6 +11,7 @@ import {
|
||||||
setEnvironment,
|
setEnvironment,
|
||||||
} from '../environment';
|
} from '../environment';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
import { createSetting } from '../util/preload';
|
||||||
|
|
||||||
const config = url.parse(window.location.toString(), true).query;
|
const config = url.parse(window.location.toString(), true).query;
|
||||||
const { locale } = config;
|
const { locale } = config;
|
||||||
|
@ -20,8 +21,14 @@ strictAssert(typeof locale === 'string', 'locale is not a string');
|
||||||
const localeMessages = ipcRenderer.sendSync('locale-data');
|
const localeMessages = ipcRenderer.sendSync('locale-data');
|
||||||
setEnvironment(parseEnvironment(config.environment));
|
setEnvironment(parseEnvironment(config.environment));
|
||||||
|
|
||||||
|
strictAssert(Boolean(window.SignalContext), 'context must be defined');
|
||||||
|
|
||||||
export const SignalWindow = {
|
export const SignalWindow = {
|
||||||
|
Settings: {
|
||||||
|
themeSetting: createSetting('themeSetting', { setter: false }),
|
||||||
|
},
|
||||||
config,
|
config,
|
||||||
|
context: window.SignalContext,
|
||||||
getAppInstance: (): string | undefined =>
|
getAppInstance: (): string | undefined =>
|
||||||
config.appInstance ? String(config.appInstance) : undefined,
|
config.appInstance ? String(config.appInstance) : undefined,
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
|
|
68
ts/windows/permissions/preload.ts
Normal file
68
ts/windows/permissions/preload.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// It is important to call this as early as possible
|
||||||
|
import '../context';
|
||||||
|
|
||||||
|
import { createSetting } from '../../util/preload';
|
||||||
|
import { SignalWindow } from '../configure';
|
||||||
|
import { PermissionsPopup } from '../../components/PermissionsPopup';
|
||||||
|
import { initialize as initializeLogging } from '../../logging/set_up_renderer_logging';
|
||||||
|
|
||||||
|
const mediaCameraPermissions = createSetting('mediaCameraPermissions', {
|
||||||
|
getter: false,
|
||||||
|
});
|
||||||
|
const mediaPermissions = createSetting('mediaPermissions', {
|
||||||
|
getter: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld(
|
||||||
|
'nativeThemeListener',
|
||||||
|
window.SignalContext.nativeThemeListener
|
||||||
|
);
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('SignalWindow', {
|
||||||
|
...SignalWindow,
|
||||||
|
renderWindow: () => {
|
||||||
|
const forCalling = SignalWindow.config.forCalling === 'true';
|
||||||
|
const forCamera = SignalWindow.config.forCamera === 'true';
|
||||||
|
|
||||||
|
let message;
|
||||||
|
if (forCalling) {
|
||||||
|
if (forCamera) {
|
||||||
|
message = SignalWindow.i18n('videoCallingPermissionNeeded');
|
||||||
|
} else {
|
||||||
|
message = SignalWindow.i18n('audioCallingPermissionNeeded');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = SignalWindow.i18n('audioPermissionNeeded');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
ipcRenderer.send('close-permissions-popup');
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
React.createElement(PermissionsPopup, {
|
||||||
|
i18n: SignalWindow.i18n,
|
||||||
|
message,
|
||||||
|
onAccept: () => {
|
||||||
|
if (!forCamera) {
|
||||||
|
mediaPermissions.setValue(true);
|
||||||
|
} else {
|
||||||
|
mediaCameraPermissions.setValue(true);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onClose,
|
||||||
|
}),
|
||||||
|
document.getElementById('app')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
initializeLogging();
|
Loading…
Add table
Reference in a new issue