Permissions popup context iso

This commit is contained in:
Josh Perez 2021-09-17 18:24:21 -04:00 committed by GitHub
parent f3715411c6
commit 7b5faa1cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 562 additions and 506 deletions

View File

@ -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
View File

@ -1103,9 +1103,15 @@ function showPermissionsPopupWindow(forCalling, forCamera) {
...defaultWebPrefs,
nodeIntegration: false,
nodeIntegrationInWorker: false,
contextIsolation: false,
contextIsolation: true,
enableRemoteModule: true,
preload: path.join(__dirname, 'permissions_popup_preload.js'),
preload: path.join(
__dirname,
'ts',
'windows',
'permissions',
'preload.js'
),
nativeWindowOpen: true,
},
parent: mainWindow,

View File

@ -6,11 +6,7 @@
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none';
child-src 'self';
connect-src 'self' https: wss:;
font-src 'self';
form-action 'self';
frame-src 'none';
img-src 'self' blob: data:;
media-src 'self' blob:;
object-src 'none';
@ -23,14 +19,13 @@
type="text/css"
/>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
<style></style>
</head>
<body class="permissions-popup"></body>
<script type="text/javascript" src="js/components.js"></script>
<script type="text/javascript" src="ts/backboneJquery.js"></script>
<script
type="text/javascript"
src="ts/shims/showConfirmationDialog.js"
></script>
<script type="text/javascript" src="js/permissions_popup_start.js"></script>
<body>
<div id="app"></div>
<script
type="application/javascript"
src="ts/windows/applyTheme.js"
></script>
<script type="application/javascript" src="ts/windows/init.js"></script>
</body>
</html>

View File

@ -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 }),
};

View 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;
}
}
}

View File

@ -69,6 +69,7 @@
@import './components/MessageAudio.scss';
@import './components/MessageDetail.scss';
@import './components/Modal.scss';
@import './components/PermissionsPopup.scss';
@import './components/Preferences.scss';
@import './components/ProfileEditor.scss';
@import './components/ReactionPickerPicker.scss';

View File

@ -1,8 +1,9 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import React from 'react';
import { LocalizerType } from '../types/Util';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
export type PropsType = {
closeAbout: () => unknown;
@ -17,21 +18,7 @@ export const About = ({
environment,
version,
}: PropsType): JSX.Element => {
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeAbout();
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [closeAbout]);
useEscapeHandling(closeAbout);
return (
<div className="About">

View File

@ -9,7 +9,7 @@ import { Inbox } from './Inbox';
import { Install } from './Install';
import { StandaloneRegistration } from './StandaloneRegistration';
import { ThemeType } from '../types/Util';
import { usePageVisibility } from '../util/hooks';
import { usePageVisibility } from '../hooks/usePageVisibility';
type PropsType = {
appView: AppViewType;

View File

@ -5,7 +5,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { Avatar, Props as AvatarProps } from './Avatar';
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import { LocalizerType } from '../types/Util';

View File

@ -18,7 +18,7 @@ import {
import { AvatarColors } from '../types/Colors';
import { SetRendererCanvasType } from '../state/ducks/calling';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
import { usePageVisibility } from '../util/hooks';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { missingCaseError } from '../util/missingCaseError';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';

View File

@ -9,7 +9,7 @@ import React, {
ReactNode,
} from 'react';
import { usePrevious } from '../util/hooks';
import { usePrevious } from '../hooks/usePrevious';
import { scrollToBottom } from '../util/scrollToBottom';
type PropsType = {

View File

@ -22,7 +22,7 @@ import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationDialog } from './ConfirmationDialog';
import { Intl } from './Intl';
import { ContactName } from './conversation/ContactName';
import { useIntersectionObserver } from '../util/hooks';
import { useIntersectionObserver } from '../hooks/useIntersectionObserver';
import { MAX_FRAME_SIZE } from '../calling/constants';
const MAX_TIME_TO_SHOW_STALE_VIDEO_FRAMES = 5000;

View File

@ -16,7 +16,7 @@ import {
} from '../types/Calling';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
import { LocalizerType } from '../types/Util';
import { usePageVisibility } from '../util/hooks';
import { usePageVisibility } from '../hooks/usePageVisibility';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant';
import * as log from '../logging/log';

View File

@ -37,7 +37,7 @@ import {
import * as OS from '../OS';
import { LocalizerType, ScrollBehavior } from '../types/Util';
import { usePrevious } from '../util/hooks';
import { usePrevious } from '../hooks/usePrevious';
import { missingCaseError } from '../util/missingCaseError';
import { ConversationList } from './ConversationList';

View File

@ -21,7 +21,7 @@ import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import { LocalizerType } from '../types/Util';
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
import { formatDuration } from '../util/formatDuration';
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import * as log from '../logging/log';
export type PropsType = {

View File

@ -10,7 +10,7 @@ import { LocalizerType } from '../types/Util';
import { ModalHost } from './ModalHost';
import { Theme } from '../util/theme';
import { getClassNamesFor } from '../util/getClassNamesFor';
import { useHasWrapped } from '../util/hooks';
import { useHasWrapped } from '../hooks/useHasWrapped';
type PropsType = {
children: ReactNode;

View File

@ -5,6 +5,7 @@ import React, { useEffect } from 'react';
import classNames from 'classnames';
import { createPortal } from 'react-dom';
import { Theme, themeClassName } from '../util/theme';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
export type PropsType = {
readonly noMouseClose?: boolean;
@ -30,25 +31,7 @@ export const ModalHost = React.memo(
};
}, []);
useEffect(() => {
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]);
useEscapeHandling(onEscape || onClose);
// 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.

View 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>
);
};

View File

@ -33,6 +33,7 @@ import {
DEFAULT_DURATIONS_SET,
format as formatExpirationTimer,
} from '../util/expirationTimer';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
type CheckboxChangeHandlerType = (value: boolean) => unknown;
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
@ -280,21 +281,7 @@ export const Preferences = ({
doneRendering();
}, [doneRendering]);
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeSettings();
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [closeSettings]);
useEscapeHandling(closeSettings);
const onZoomSelectChange = useCallback(
(value: string) => {

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { useRestoreFocus } from '../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import { LocalizerType } from '../types/Util';
export type Props = {

View File

@ -15,7 +15,7 @@ import {
getCallingIcon,
getCallingNotificationText,
} from '../../util/callingNotification';
import { usePrevious } from '../../util/hooks';
import { usePrevious } from '../../hooks/usePrevious';
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import type { TimelineItemType } from './TimelineItem';

View File

@ -6,7 +6,7 @@ import classNames from 'classnames';
import { Modal } from '../Modal';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { LocalizerType } from '../../types/Util';

View File

@ -9,7 +9,7 @@ import { Modal } from '../Modal';
import { Intl } from '../Intl';
import { Emojify } from './Emojify';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { LocalizerType } from '../../types/Util';

View File

@ -4,7 +4,7 @@
import * as React from 'react';
import { convertShortName } from '../emoji/lib';
import { Props as EmojiPickerProps } from '../emoji/EmojiPicker';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { LocalizerType } from '../../types/Util';
import { canCustomizePreferredReactions } from '../../util/canCustomizePreferredReactions';
import {

View File

@ -7,9 +7,10 @@ import classNames from 'classnames';
import { ContactName } from './ContactName';
import { Avatar, Props as AvatarProps } from '../Avatar';
import { Emoji } from '../emoji/Emoji';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { ConversationType } from '../../state/ducks/conversations';
import { emojiToData, EmojiData } from '../emoji/lib';
import { useEscapeHandling } from '../../hooks/useEscapeHandling';
export type Reaction = {
emoji: string;
@ -124,20 +125,9 @@ export const ReactionViewer = React.forwardRef<HTMLDivElement, Props>(
selectedReactionCategory,
setSelectedReactionCategory,
] = React.useState(pickedReaction || 'all');
// Handle escape key
React.useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (onClose && e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [onClose]);
useEscapeHandling(onClose);
// Focus first button and restore focus on unmount
const [focusRef] = useRestoreFocus();

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
import { LocalizerType } from '../../types/Util';

View File

@ -10,7 +10,7 @@ import { ConfirmationDialog } from '../ConfirmationDialog';
import { LocalizerType } from '../../types/Util';
import { StickerPackType } from '../../state/ducks/stickers';
import { Spinner } from '../Spinner';
import { useRestoreFocus } from '../../util/hooks/useRestoreFocus';
import { useRestoreFocus } from '../../hooks/useRestoreFocus';
export type OwnProps = {
readonly onClose: () => unknown;

View File

@ -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';
}
}

View 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,
};
}

View File

@ -2,7 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { Bytes } from './Bytes';
import { NativeThemeListener, MinimalIPC } from './NativeThemeListener';
import {
createNativeThemeListener,
MinimalIPC,
} from './createNativeThemeListener';
export class Context {
public readonly bytes = new Bytes();
@ -10,6 +13,6 @@ export class Context {
public readonly nativeThemeListener;
constructor(ipc: MinimalIPC) {
this.nativeThemeListener = new NativeThemeListener(ipc, window);
this.nativeThemeListener = createNativeThemeListener(ipc, window);
}
}

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect } from 'react';
import { usePrevious } from '../util/hooks';
import { usePrevious } from './usePrevious';
type RemoteParticipant = {
hasRemoteVideo: boolean;

View 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]);
};

View 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
View 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];
}

View 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];
}

View 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
View 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;
}

View File

@ -1,7 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useBoundActions } from '../../util/hooks';
import { useBoundActions } from '../../hooks/useBoundActions';
import {
SwitchToAssociatedViewActionType,

View File

@ -5,7 +5,7 @@ import { take, uniq } from 'lodash';
import { ThunkAction } from 'redux-thunk';
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import dataInterface from '../../sql/Client';
import { useBoundActions } from '../../util/hooks';
import { useBoundActions } from '../../hooks/useBoundActions';
const { updateEmojiUsage } = dataInterface;

View File

@ -6,7 +6,7 @@ import { v4 as getGuid } from 'uuid';
import { ThunkAction } from 'redux-thunk';
import { StateType as RootStateType } from '../reducer';
import * as storageShim from '../../shims/storage';
import { useBoundActions } from '../../util/hooks';
import { useBoundActions } from '../../hooks/useBoundActions';
import {
ConversationColors,
ConversationColorType,

View File

@ -6,7 +6,7 @@ import { omit } from 'lodash';
import * as log from '../../logging/log';
import * as Errors from '../../types/errors';
import { replaceIndex } from '../../util/replaceIndex';
import { useBoundActions } from '../../util/hooks';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { StateType as RootStateType } from '../reducer';
import { DEFAULT_PREFERRED_REACTION_EMOJI_SHORT_NAMES } from '../../reactions/constants';
import { getPreferredReactionEmoji } from '../../reactions/preferredReactionEmoji';

View File

@ -5,10 +5,10 @@ import { assert } from 'chai';
import { EventEmitter } from 'events';
import {
NativeThemeListener,
createNativeThemeListener,
MinimalIPC,
SystemThemeHolder,
} from '../../context/NativeThemeListener';
} from '../../context/createNativeThemeListener';
import { NativeThemeState } from '../../types/NativeThemeNotifier.d';
class FakeIPC extends EventEmitter implements MinimalIPC {
@ -26,7 +26,7 @@ describe('NativeThemeListener', () => {
const holder: SystemThemeHolder = { systemTheme: 'dark' };
it('syncs the initial native theme', () => {
const dark = new NativeThemeListener(
const dark = createNativeThemeListener(
new FakeIPC({
shouldUseDarkColors: true,
}),
@ -34,9 +34,9 @@ describe('NativeThemeListener', () => {
);
assert.strictEqual(holder.systemTheme, 'dark');
assert.isTrue(dark.theme.shouldUseDarkColors);
assert.strictEqual(dark.getSystemTheme(), 'dark');
const light = new NativeThemeListener(
const light = createNativeThemeListener(
new FakeIPC({
shouldUseDarkColors: false,
}),
@ -44,7 +44,7 @@ describe('NativeThemeListener', () => {
);
assert.strictEqual(holder.systemTheme, 'light');
assert.isFalse(light.theme.shouldUseDarkColors);
assert.strictEqual(light.getSystemTheme(), 'light');
});
it('should react to native theme changes', () => {
@ -52,14 +52,14 @@ describe('NativeThemeListener', () => {
shouldUseDarkColors: true,
});
const listener = new NativeThemeListener(ipc, holder);
const listener = createNativeThemeListener(ipc, holder);
ipc.emit('native-theme:changed', null, <NativeThemeState>{
shouldUseDarkColors: false,
});
assert.strictEqual(holder.systemTheme, 'light');
assert.isFalse(listener.theme.shouldUseDarkColors);
assert.strictEqual(listener.getSystemTheme(), 'light');
});
it('should notify subscribers of native theme changes', done => {
@ -67,7 +67,7 @@ describe('NativeThemeListener', () => {
shouldUseDarkColors: true,
});
const listener = new NativeThemeListener(ipc, holder);
const listener = createNativeThemeListener(ipc, holder);
listener.subscribe(state => {
assert.isFalse(state.shouldUseDarkColors);

View File

@ -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];
}

View File

@ -144,22 +144,6 @@
"reasonCategory": "falseMatch",
"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-$(",
"path": "js/views/key_verification_view.js",
@ -12955,6 +12939,49 @@
"updated": "2019-11-21T06:13:49.384Z",
"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(",
"path": "ts/logging/debuglogs.js",
@ -13206,65 +13233,6 @@
"updated": "2021-08-18T18:22:55.307Z",
"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-$(",
"path": "ts/views/debug_log_view.js",

7
ts/window.d.ts vendored
View File

@ -116,10 +116,11 @@ import { UUID } from './types/UUID';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
import { CI } from './CI';
import { IPCEventsType } from './util/createIPCEvents';
import { IPCEventsType, IPCEventsValuesType } from './util/createIPCEvents';
import { ConversationView } from './views/conversation_view';
import { DebugLogView } from './views/debug_log_view';
import { LoggerType } from './types/Logging';
import { SettingType } from './util/preload';
export { Long } from 'long';
@ -496,7 +497,11 @@ declare global {
// Context Isolation
SignalWindow: {
Settings: {
themeSetting: SettingType<IPCEventsValuesType['themeSetting']>;
};
config: string;
context: SignalContext;
getAppInstance: () => string | undefined;
getEnvironment: () => string;
getVersion: () => string;

21
ts/windows/applyTheme.ts Normal file
View 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();
});

View File

@ -11,6 +11,7 @@ import {
setEnvironment,
} from '../environment';
import { strictAssert } from '../util/assert';
import { createSetting } from '../util/preload';
const config = url.parse(window.location.toString(), true).query;
const { locale } = config;
@ -20,8 +21,14 @@ strictAssert(typeof locale === 'string', 'locale is not a string');
const localeMessages = ipcRenderer.sendSync('locale-data');
setEnvironment(parseEnvironment(config.environment));
strictAssert(Boolean(window.SignalContext), 'context must be defined');
export const SignalWindow = {
Settings: {
themeSetting: createSetting('themeSetting', { setter: false }),
},
config,
context: window.SignalContext,
getAppInstance: (): string | undefined =>
config.appInstance ? String(config.appInstance) : undefined,
getEnvironment,

View 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();