Use patched frameless-titlebar on Windows
This commit is contained in:
parent
79c52847cd
commit
5634601554
54 changed files with 1343 additions and 323 deletions
|
@ -1,15 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { TitleBarContainer } from './TitleBarContainer';
|
||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||
|
||||
export type PropsType = {
|
||||
closeAbout: () => unknown;
|
||||
environment: string;
|
||||
i18n: LocalizerType;
|
||||
version: string;
|
||||
platform: string;
|
||||
executeMenuRole: ExecuteMenuRoleType;
|
||||
};
|
||||
|
||||
export const About = ({
|
||||
|
@ -17,34 +23,45 @@ export const About = ({
|
|||
i18n,
|
||||
environment,
|
||||
version,
|
||||
platform,
|
||||
executeMenuRole,
|
||||
}: PropsType): JSX.Element => {
|
||||
useEscapeHandling(closeAbout);
|
||||
|
||||
return (
|
||||
<div className="About">
|
||||
<div className="module-splash-screen">
|
||||
<div className="module-splash-screen__logo module-img--150" />
|
||||
const theme = useTheme();
|
||||
|
||||
<div className="version">{version}</div>
|
||||
<div className="environment">{environment}</div>
|
||||
<div>
|
||||
<a href="https://signal.org">signal.org</a>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<a
|
||||
className="acknowledgments"
|
||||
href="https://github.com/signalapp/Signal-Desktop/blob/main/ACKNOWLEDGMENTS.md"
|
||||
>
|
||||
{i18n('softwareAcknowledgments')}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a className="privacy" href="https://signal.org/legal">
|
||||
{i18n('privacyPolicy')}
|
||||
</a>
|
||||
return (
|
||||
<TitleBarContainer
|
||||
platform={platform}
|
||||
theme={theme}
|
||||
executeMenuRole={executeMenuRole}
|
||||
title={i18n('aboutSignalDesktop')}
|
||||
>
|
||||
<div className="About">
|
||||
<div className="module-splash-screen">
|
||||
<div className="module-splash-screen__logo module-img--150" />
|
||||
|
||||
<div className="version">{version}</div>
|
||||
<div className="environment">{environment}</div>
|
||||
<div>
|
||||
<a href="https://signal.org">signal.org</a>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<a
|
||||
className="acknowledgments"
|
||||
href="https://github.com/signalapp/Signal-Desktop/blob/main/ACKNOWLEDGMENTS.md"
|
||||
>
|
||||
{i18n('softwareAcknowledgments')}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a className="privacy" href="https://signal.org/legal">
|
||||
{i18n('privacyPolicy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TitleBarContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
|
@ -11,11 +11,16 @@ import { Inbox } from './Inbox';
|
|||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
import { useReducedMotion } from '../hooks/useReducedMotion';
|
||||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||
import { TitleBarContainer } from './TitleBarContainer';
|
||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||
|
||||
type PropsType = {
|
||||
appView: AppViewType;
|
||||
localeMessages: LocaleMessagesType;
|
||||
openInbox: () => void;
|
||||
registerSingleDevice: (number: string, code: string) => Promise<void>;
|
||||
renderCallManager: () => JSX.Element;
|
||||
|
@ -28,6 +33,14 @@ type PropsType = {
|
|||
token: string
|
||||
) => Promise<void>;
|
||||
theme: ThemeType;
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
menuOptions: MenuOptionsType;
|
||||
platform: string;
|
||||
|
||||
executeMenuRole: ExecuteMenuRoleType;
|
||||
executeMenuAction: (action: MenuActionType) => void;
|
||||
titleBarDoubleClick: () => void;
|
||||
} & ComponentProps<typeof Inbox>;
|
||||
|
||||
export const App = ({
|
||||
|
@ -39,6 +52,11 @@ export const App = ({
|
|||
i18n,
|
||||
isCustomizingPreferredReactions,
|
||||
isShowingStoriesView,
|
||||
isMaximized,
|
||||
isFullScreen,
|
||||
menuOptions,
|
||||
platform,
|
||||
localeMessages,
|
||||
renderCallManager,
|
||||
renderCustomizingPreferredReactionsModal,
|
||||
renderGlobalModalContainer,
|
||||
|
@ -49,6 +67,9 @@ export const App = ({
|
|||
registerSingleDevice,
|
||||
theme,
|
||||
verifyConversationsStoppingSend,
|
||||
executeMenuAction,
|
||||
executeMenuRole,
|
||||
titleBarDoubleClick,
|
||||
}: PropsType): JSX.Element => {
|
||||
let contents;
|
||||
|
||||
|
@ -113,17 +134,31 @@ export const App = ({
|
|||
}, [prefersReducedMotion]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
App: true,
|
||||
'light-theme': theme === ThemeType.light,
|
||||
'dark-theme': theme === ThemeType.dark,
|
||||
})}
|
||||
<TitleBarContainer
|
||||
title="Signal"
|
||||
theme={theme}
|
||||
isMaximized={isMaximized}
|
||||
isFullScreen={isFullScreen}
|
||||
platform={platform}
|
||||
hasMenu
|
||||
localeMessages={localeMessages}
|
||||
menuOptions={menuOptions}
|
||||
executeMenuRole={executeMenuRole}
|
||||
executeMenuAction={executeMenuAction}
|
||||
titleBarDoubleClick={titleBarDoubleClick}
|
||||
>
|
||||
{renderGlobalModalContainer()}
|
||||
{renderCallManager()}
|
||||
{isShowingStoriesView && renderStories()}
|
||||
{contents}
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
App: true,
|
||||
'light-theme': theme === ThemeType.light,
|
||||
'dark-theme': theme === ThemeType.dark,
|
||||
})}
|
||||
>
|
||||
{renderGlobalModalContainer()}
|
||||
{renderCallManager()}
|
||||
{isShowingStoriesView && renderStories()}
|
||||
{contents}
|
||||
</div>
|
||||
</TitleBarContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,8 @@ const createProps = (): PropsType => ({
|
|||
await sleep(5000);
|
||||
return 'https://picsum.photos/1800/900';
|
||||
},
|
||||
executeMenuRole: action('executeMenuRole'),
|
||||
platform: 'win32',
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -10,10 +10,13 @@ import type { LocalizerType } from '../types/Util';
|
|||
import { Spinner } from './Spinner';
|
||||
import { ToastDebugLogError } from './ToastDebugLogError';
|
||||
import { ToastLinkCopied } from './ToastLinkCopied';
|
||||
import { TitleBarContainer } from './TitleBarContainer';
|
||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||
import { ToastLoadingFullLogs } from './ToastLoadingFullLogs';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { createSupportUrl } from '../util/createSupportUrl';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
|
||||
enum LoadState {
|
||||
NotStarted,
|
||||
|
@ -28,6 +31,8 @@ export type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
fetchLogs: () => Promise<string>;
|
||||
uploadLogs: (logs: string) => Promise<string>;
|
||||
platform: string;
|
||||
executeMenuRole: ExecuteMenuRoleType;
|
||||
};
|
||||
|
||||
enum ToastType {
|
||||
|
@ -42,6 +47,8 @@ export const DebugLogWindow = ({
|
|||
i18n,
|
||||
fetchLogs,
|
||||
uploadLogs,
|
||||
platform,
|
||||
executeMenuRole,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted);
|
||||
const [logText, setLogText] = useState<string | undefined>();
|
||||
|
@ -49,6 +56,8 @@ export const DebugLogWindow = ({
|
|||
const [textAreaValue, setTextAreaValue] = useState<string>(i18n('loading'));
|
||||
const [toastType, setToastType] = useState<ToastType | undefined>();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEscapeHandling(closeWindow);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -135,32 +144,41 @@ export const DebugLogWindow = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">{i18n('debugLogSuccess')}</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogSuccessNextSteps')}
|
||||
</p>
|
||||
<TitleBarContainer
|
||||
platform={platform}
|
||||
theme={theme}
|
||||
executeMenuRole={executeMenuRole}
|
||||
title={i18n('debugLog')}
|
||||
>
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">
|
||||
{i18n('debugLogSuccess')}
|
||||
</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogSuccessNextSteps')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
<input
|
||||
className="DebugLogWindow__link"
|
||||
readOnly
|
||||
type="text"
|
||||
value={publicLogURL}
|
||||
/>
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
onClick={() => openLinkInWebBrowser(supportURL)}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('reportIssue')}
|
||||
</Button>
|
||||
<Button onClick={copyLog}>{i18n('debugLogCopy')}</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
<input
|
||||
className="DebugLogWindow__link"
|
||||
readOnly
|
||||
type="text"
|
||||
value={publicLogURL}
|
||||
/>
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
onClick={() => openLinkInWebBrowser(supportURL)}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('reportIssue')}
|
||||
</Button>
|
||||
<Button onClick={copyLog}>{i18n('debugLogCopy')}</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
</TitleBarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -170,43 +188,50 @@ export const DebugLogWindow = ({
|
|||
loadState === LoadState.Started || loadState === LoadState.Submitting;
|
||||
|
||||
return (
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogExplanation')}
|
||||
</p>
|
||||
<TitleBarContainer
|
||||
platform={platform}
|
||||
theme={theme}
|
||||
executeMenuRole={executeMenuRole}
|
||||
title={i18n('debugLog')}
|
||||
>
|
||||
<div className="DebugLogWindow">
|
||||
<div>
|
||||
<div className="DebugLogWindow__title">{i18n('submitDebugLog')}</div>
|
||||
<p className="DebugLogWindow__subtitle">
|
||||
{i18n('debugLogExplanation')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
{isLoading ? (
|
||||
<Spinner svgSize="normal" />
|
||||
) : (
|
||||
<textarea
|
||||
className="DebugLogWindow__textarea"
|
||||
readOnly
|
||||
rows={5}
|
||||
spellCheck={false}
|
||||
value={textAreaValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (logText) {
|
||||
downloadLog(logText);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('debugLogSave')}
|
||||
</Button>
|
||||
<Button disabled={!canSubmit} onClick={handleSubmit}>
|
||||
{i18n('submit')}
|
||||
</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
<div className="DebugLogWindow__container">
|
||||
{isLoading ? (
|
||||
<Spinner svgSize="normal" />
|
||||
) : (
|
||||
<textarea
|
||||
className="DebugLogWindow__textarea"
|
||||
readOnly
|
||||
rows={5}
|
||||
spellCheck={false}
|
||||
value={textAreaValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="DebugLogWindow__footer">
|
||||
<Button
|
||||
disabled={!canSave}
|
||||
onClick={() => {
|
||||
if (logText) {
|
||||
downloadLog(logText);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Secondary}
|
||||
>
|
||||
{i18n('debugLogSave')}
|
||||
</Button>
|
||||
<Button disabled={!canSubmit} onClick={handleSubmit}>
|
||||
{i18n('submit')}
|
||||
</Button>
|
||||
</div>
|
||||
{toastElement}
|
||||
</div>
|
||||
</TitleBarContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -103,8 +103,19 @@ export const ModalHost = React.memo(
|
|||
useFocusTrap ? (
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
// This is alright because the overlay covers the entire screen
|
||||
allowOutsideClick: false,
|
||||
allowOutsideClick: ({ target }) => {
|
||||
if (!target || !(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const titleBar = document.querySelector(
|
||||
'.TitleBarContainer__title'
|
||||
);
|
||||
if (titleBar?.contains(target)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{modalContent}
|
||||
|
|
|
@ -156,6 +156,9 @@ const createProps = (): PropsType => ({
|
|||
onZoomFactorChange: action('onZoomFactorChange'),
|
||||
|
||||
i18n,
|
||||
|
||||
executeMenuRole: action('executeMenuRole'),
|
||||
platform: 'win32',
|
||||
});
|
||||
|
||||
export default {
|
||||
|
|
|
@ -29,6 +29,8 @@ import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
|
|||
import { PhoneNumberSharingMode } from '../util/phoneNumberSharingMode';
|
||||
import { Select } from './Select';
|
||||
import { Spinner } from './Spinner';
|
||||
import { TitleBarContainer } from './TitleBarContainer';
|
||||
import type { ExecuteMenuRoleType } from './TitleBarContainer';
|
||||
import { getCustomColorStyle } from '../util/getCustomColorStyle';
|
||||
import {
|
||||
DEFAULT_DURATIONS_IN_SECONDS,
|
||||
|
@ -37,6 +39,7 @@ import {
|
|||
} from '../util/expirationTimer';
|
||||
import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
||||
import { useUniqueId } from '../hooks/useUniqueId';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||
|
@ -99,6 +102,8 @@ export type PropsType = {
|
|||
value: CustomColorType;
|
||||
}
|
||||
) => unknown;
|
||||
platform: string;
|
||||
executeMenuRole: ExecuteMenuRoleType;
|
||||
|
||||
// Limited support features
|
||||
isAudioNotificationsSupported: boolean;
|
||||
|
@ -193,6 +198,7 @@ export const Preferences = ({
|
|||
doDeleteAllData,
|
||||
doneRendering,
|
||||
editCustomColor,
|
||||
executeMenuRole,
|
||||
getConversationsWithCustomColor,
|
||||
hasAudioNotifications,
|
||||
hasAutoDownloadUpdate,
|
||||
|
@ -250,6 +256,7 @@ export const Preferences = ({
|
|||
onThemeChange,
|
||||
onUniversalExpireTimerChange,
|
||||
onZoomFactorChange,
|
||||
platform,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
resetAllChatColors,
|
||||
|
@ -273,6 +280,7 @@ export const Preferences = ({
|
|||
const [nowSyncing, setNowSyncing] = useState(false);
|
||||
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
|
||||
useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
doneRendering();
|
||||
|
@ -1017,78 +1025,85 @@ export const Preferences = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="Preferences">
|
||||
<div className="Preferences__page-selector">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--general': true,
|
||||
'Preferences__button--selected': page === Page.General,
|
||||
})}
|
||||
onClick={() => setPage(Page.General)}
|
||||
>
|
||||
{i18n('Preferences__button--general')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--appearance': true,
|
||||
'Preferences__button--selected':
|
||||
page === Page.Appearance || page === Page.ChatColor,
|
||||
})}
|
||||
onClick={() => setPage(Page.Appearance)}
|
||||
>
|
||||
{i18n('Preferences__button--appearance')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--chats': true,
|
||||
'Preferences__button--selected': page === Page.Chats,
|
||||
})}
|
||||
onClick={() => setPage(Page.Chats)}
|
||||
>
|
||||
{i18n('Preferences__button--chats')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--calls': true,
|
||||
'Preferences__button--selected': page === Page.Calls,
|
||||
})}
|
||||
onClick={() => setPage(Page.Calls)}
|
||||
>
|
||||
{i18n('Preferences__button--calls')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--notifications': true,
|
||||
'Preferences__button--selected': page === Page.Notifications,
|
||||
})}
|
||||
onClick={() => setPage(Page.Notifications)}
|
||||
>
|
||||
{i18n('Preferences__button--notifications')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--privacy': true,
|
||||
'Preferences__button--selected': page === Page.Privacy,
|
||||
})}
|
||||
onClick={() => setPage(Page.Privacy)}
|
||||
>
|
||||
{i18n('Preferences__button--privacy')}
|
||||
</button>
|
||||
<TitleBarContainer
|
||||
platform={platform}
|
||||
theme={theme}
|
||||
executeMenuRole={executeMenuRole}
|
||||
title={i18n('signalDesktopPreferences')}
|
||||
>
|
||||
<div className="Preferences">
|
||||
<div className="Preferences__page-selector">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--general': true,
|
||||
'Preferences__button--selected': page === Page.General,
|
||||
})}
|
||||
onClick={() => setPage(Page.General)}
|
||||
>
|
||||
{i18n('Preferences__button--general')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--appearance': true,
|
||||
'Preferences__button--selected':
|
||||
page === Page.Appearance || page === Page.ChatColor,
|
||||
})}
|
||||
onClick={() => setPage(Page.Appearance)}
|
||||
>
|
||||
{i18n('Preferences__button--appearance')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--chats': true,
|
||||
'Preferences__button--selected': page === Page.Chats,
|
||||
})}
|
||||
onClick={() => setPage(Page.Chats)}
|
||||
>
|
||||
{i18n('Preferences__button--chats')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--calls': true,
|
||||
'Preferences__button--selected': page === Page.Calls,
|
||||
})}
|
||||
onClick={() => setPage(Page.Calls)}
|
||||
>
|
||||
{i18n('Preferences__button--calls')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--notifications': true,
|
||||
'Preferences__button--selected': page === Page.Notifications,
|
||||
})}
|
||||
onClick={() => setPage(Page.Notifications)}
|
||||
>
|
||||
{i18n('Preferences__button--notifications')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--privacy': true,
|
||||
'Preferences__button--selected': page === Page.Privacy,
|
||||
})}
|
||||
onClick={() => setPage(Page.Privacy)}
|
||||
>
|
||||
{i18n('Preferences__button--privacy')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="Preferences__settings-pane">{settings}</div>
|
||||
</div>
|
||||
<div className="Preferences__settings-pane">{settings}</div>
|
||||
</div>
|
||||
</TitleBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
185
ts/components/TitleBarContainer.tsx
Normal file
185
ts/components/TitleBarContainer.tsx
Normal file
|
@ -0,0 +1,185 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import TitleBar from '@indutny/frameless-titlebar';
|
||||
import type { MenuItem } from '@indutny/frameless-titlebar';
|
||||
import type { MenuItemConstructorOptions } from 'electron';
|
||||
|
||||
import { createTemplate } from '../../app/menu';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { LocaleMessagesType } from '../types/I18N';
|
||||
import type { MenuOptionsType, MenuActionType } from '../types/menu';
|
||||
|
||||
export type MenuPropsType = Readonly<{
|
||||
hasMenu: true;
|
||||
localeMessages: LocaleMessagesType;
|
||||
menuOptions: MenuOptionsType;
|
||||
executeMenuAction: (action: MenuActionType) => void;
|
||||
}>;
|
||||
|
||||
export type ExecuteMenuRoleType = (
|
||||
role: MenuItemConstructorOptions['role']
|
||||
) => void;
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
title: string;
|
||||
theme: ThemeType;
|
||||
isMaximized?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
platform: string;
|
||||
executeMenuRole: ExecuteMenuRoleType;
|
||||
titleBarDoubleClick?: () => void;
|
||||
children: ReactNode;
|
||||
}> &
|
||||
(MenuPropsType | { hasMenu?: false });
|
||||
|
||||
// Windows only
|
||||
const ROLE_TO_ACCELERATOR = new Map<
|
||||
MenuItemConstructorOptions['role'],
|
||||
string
|
||||
>();
|
||||
ROLE_TO_ACCELERATOR.set('undo', 'CmdOrCtrl+Z');
|
||||
ROLE_TO_ACCELERATOR.set('redo', 'CmdOrCtrl+Y');
|
||||
ROLE_TO_ACCELERATOR.set('cut', 'CmdOrCtrl+X');
|
||||
ROLE_TO_ACCELERATOR.set('copy', 'CmdOrCtrl+C');
|
||||
ROLE_TO_ACCELERATOR.set('paste', 'CmdOrCtrl+V');
|
||||
ROLE_TO_ACCELERATOR.set('pasteAndMatchStyle', 'CmdOrCtrl+Shift+V');
|
||||
ROLE_TO_ACCELERATOR.set('selectAll', 'CmdOrCtrl+A');
|
||||
ROLE_TO_ACCELERATOR.set('resetZoom', 'CmdOrCtrl+0');
|
||||
ROLE_TO_ACCELERATOR.set('zoomIn', 'CmdOrCtrl+=');
|
||||
ROLE_TO_ACCELERATOR.set('zoomOut', 'CmdOrCtrl+-');
|
||||
ROLE_TO_ACCELERATOR.set('togglefullscreen', 'F11');
|
||||
ROLE_TO_ACCELERATOR.set('toggleDevTools', 'CmdOrCtrl+Shift+I');
|
||||
ROLE_TO_ACCELERATOR.set('minimize', 'CmdOrCtrl+M');
|
||||
|
||||
function convertMenu(
|
||||
menuList: ReadonlyArray<MenuItemConstructorOptions>,
|
||||
executeMenuRole: (role: MenuItemConstructorOptions['role']) => void
|
||||
): Array<MenuItem> {
|
||||
return menuList.map(item => {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
accelerator: originalAccelerator,
|
||||
click: originalClick,
|
||||
submenu: originalSubmenu,
|
||||
role,
|
||||
} = item;
|
||||
let submenu: Array<MenuItem> | undefined;
|
||||
|
||||
if (Array.isArray(originalSubmenu)) {
|
||||
submenu = convertMenu(originalSubmenu, executeMenuRole);
|
||||
} else if (originalSubmenu) {
|
||||
throw new Error('Non-array submenu is not supported');
|
||||
}
|
||||
|
||||
let click: (() => unknown) | undefined;
|
||||
if (originalClick) {
|
||||
if (role) {
|
||||
throw new Error(`Menu item: ${label} has both click and role`);
|
||||
}
|
||||
|
||||
// We don't use arguments in app/menu.ts
|
||||
click = originalClick as () => unknown;
|
||||
} else if (role) {
|
||||
click = () => executeMenuRole(role);
|
||||
}
|
||||
|
||||
let accelerator: string | undefined;
|
||||
if (originalAccelerator) {
|
||||
accelerator = originalAccelerator.toString();
|
||||
} else if (role) {
|
||||
accelerator = ROLE_TO_ACCELERATOR.get(role);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
accelerator,
|
||||
click,
|
||||
submenu,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const TitleBarContainer = (props: PropsType): JSX.Element => {
|
||||
const {
|
||||
title,
|
||||
theme,
|
||||
isMaximized,
|
||||
isFullScreen,
|
||||
executeMenuRole,
|
||||
titleBarDoubleClick,
|
||||
children,
|
||||
platform,
|
||||
hasMenu,
|
||||
} = props;
|
||||
|
||||
if (platform !== 'win32' || isFullScreen) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
let maybeMenu: Array<MenuItem> | undefined;
|
||||
if (hasMenu) {
|
||||
const { localeMessages, menuOptions, executeMenuAction } = props;
|
||||
|
||||
const menuTemplate = createTemplate(
|
||||
{
|
||||
...menuOptions,
|
||||
|
||||
// actions
|
||||
forceUpdate: () => executeMenuAction('forceUpdate'),
|
||||
openContactUs: () => executeMenuAction('openContactUs'),
|
||||
openForums: () => executeMenuAction('openForums'),
|
||||
openJoinTheBeta: () => executeMenuAction('openJoinTheBeta'),
|
||||
openReleaseNotes: () => executeMenuAction('openReleaseNotes'),
|
||||
openSupportPage: () => executeMenuAction('openSupportPage'),
|
||||
setupAsNewDevice: () => executeMenuAction('setupAsNewDevice'),
|
||||
setupAsStandalone: () => executeMenuAction('setupAsStandalone'),
|
||||
showAbout: () => executeMenuAction('showAbout'),
|
||||
showDebugLog: () => executeMenuAction('showDebugLog'),
|
||||
showKeyboardShortcuts: () => executeMenuAction('showKeyboardShortcuts'),
|
||||
showSettings: () => executeMenuAction('showSettings'),
|
||||
showStickerCreator: () => executeMenuAction('showStickerCreator'),
|
||||
showWindow: () => executeMenuAction('showWindow'),
|
||||
},
|
||||
localeMessages
|
||||
);
|
||||
|
||||
maybeMenu = convertMenu(menuTemplate, executeMenuRole);
|
||||
}
|
||||
|
||||
const titleBarTheme = {
|
||||
bar: {
|
||||
palette:
|
||||
theme === ThemeType.light ? ('light' as const) : ('dark' as const),
|
||||
},
|
||||
|
||||
// Hide overlay
|
||||
menu: {
|
||||
overlay: {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="TitleBarContainer">
|
||||
<TitleBar
|
||||
className="TitleBarContainer__title"
|
||||
platform={platform}
|
||||
title={title}
|
||||
iconSrc="images/icon_32.png"
|
||||
theme={titleBarTheme}
|
||||
maximized={isMaximized}
|
||||
menu={maybeMenu}
|
||||
onDoubleClick={titleBarDoubleClick}
|
||||
hideControls
|
||||
/>
|
||||
|
||||
<div className="TitleBarContainer__content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue