Titlebar fixes

This commit is contained in:
Fedor Indutny 2022-07-05 09:44:53 -07:00 committed by GitHub
parent f273333046
commit f92be05b15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 225 additions and 154 deletions

View File

@ -9,6 +9,9 @@
type="text/css" type="text/css"
/> />
<script> <script>
// eslint-disable-next-line
const noop = () => {};
window.SignalWindow = window.SignalWindow || {}; window.SignalWindow = window.SignalWindow || {};
window.SignalWindow.log = { window.SignalWindow.log = {
fatal: console.error.bind(console), fatal: console.error.bind(console),
@ -19,19 +22,27 @@
trace: console.trace.bind(console), trace: console.trace.bind(console),
}; };
window.SignalContext = { window.SignalContext = {
activeWindowService: {
isActive: () => true;
registerForActive: noop,
unregisterForActive: noop,
registerForChange: noop,
unregisterForChange: noop,
},
nativeThemeListener: { nativeThemeListener: {
getSystemValue: async () => 'light', getSystemValue: async () => 'light',
subscribe: () => {}, subscribe: noop,
unsubscribe: () => {}, unsubscribe: noop,
}, },
Settings: { Settings: {
themeSetting: { themeSetting: {
getValue: async () => 'light', getValue: async () => 'light',
}, },
waitForChange: () => {}, waitForChange: noop,
}, },
OS: { OS: {
isWindows11: () => false, hasCustomTitleBar: () => false,
}, },
}; };
</script> </script>

View File

@ -34,6 +34,7 @@ if (getEnvironment() === Environment.Production) {
process.env.SUPPRESS_NO_CONFIG_WARNING = ''; process.env.SUPPRESS_NO_CONFIG_WARNING = '';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = ''; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '';
process.env.SIGNAL_ENABLE_HTTP = ''; process.env.SIGNAL_ENABLE_HTTP = '';
process.env.CUSTOM_TITLEBAR = '';
} }
// We load config after we've made our modifications to NODE_ENV // We load config after we've made our modifications to NODE_ENV

View File

@ -432,6 +432,7 @@ async function prepareUrl(
// Only used by the main window // Only used by the main window
isMainWindowFullScreen: Boolean(mainWindow?.isFullScreen()), isMainWindowFullScreen: Boolean(mainWindow?.isFullScreen()),
isMainWindowMaximized: Boolean(mainWindow?.isMaximized()),
// Only for tests // Only for tests
argv: JSON.stringify(process.argv), argv: JSON.stringify(process.argv),
@ -499,6 +500,17 @@ function handleCommonWindowEvents(
activeWindows.add(window); activeWindows.add(window);
window.on('closed', () => activeWindows.delete(window)); window.on('closed', () => activeWindows.delete(window));
const setWindowFocus = () => {
window.webContents.send('set-window-focus', window.isFocused());
};
window.on('focus', setWindowFocus);
window.on('blur', setWindowFocus);
window.once('ready-to-show', setWindowFocus);
// This is a fallback in case we drop an event for some reason.
const focusInterval = setInterval(setWindowFocus, 10000);
window.on('closed', () => clearInterval(focusInterval));
// Works only for mainWindow because it has `enablePreferredSizeMode` // Works only for mainWindow because it has `enablePreferredSizeMode`
let lastZoomFactor = window.webContents.getZoomFactor(); let lastZoomFactor = window.webContents.getZoomFactor();
const onZoomChanged = () => { const onZoomChanged = () => {
@ -600,12 +612,12 @@ const mainTitleBarStyle =
? ('default' as const) ? ('default' as const)
: ('hidden' as const); : ('hidden' as const);
const nonMainTitleBarStyle = OS.isWindows() const nonMainTitleBarStyle = OS.hasCustomTitleBar()
? ('hidden' as const) ? ('hidden' as const)
: ('default' as const); : ('default' as const);
async function getTitleBarOverlay(): Promise<TitleBarOverlayOptions | false> { async function getTitleBarOverlay(): Promise<TitleBarOverlayOptions | false> {
if (!OS.isWindows()) { if (!OS.hasCustomTitleBar()) {
return false; return false;
} }
@ -782,18 +794,6 @@ async function createWindow() {
mainWindow.on('resize', captureWindowStats); mainWindow.on('resize', captureWindowStats);
mainWindow.on('move', captureWindowStats); mainWindow.on('move', captureWindowStats);
const setWindowFocus = () => {
if (!mainWindow) {
return;
}
mainWindow.webContents.send('set-window-focus', mainWindow.isFocused());
};
mainWindow.on('focus', setWindowFocus);
mainWindow.on('blur', setWindowFocus);
mainWindow.once('ready-to-show', setWindowFocus);
// This is a fallback in case we drop an event for some reason.
setInterval(setWindowFocus, 10000);
if (getEnvironment() === Environment.Test) { if (getEnvironment() === Environment.Test) {
mainWindow.loadURL(await prepareFileUrl([__dirname, '../test/index.html'])); mainWindow.loadURL(await prepareFileUrl([__dirname, '../test/index.html']));
} else { } else {

View File

@ -16,15 +16,13 @@ import type { ExecuteMenuRoleType } from '../../ts/components/TitleBarContainer'
import { useTheme } from '../../ts/hooks/useTheme'; import { useTheme } from '../../ts/hooks/useTheme';
export type AppPropsType = Readonly<{ export type AppPropsType = Readonly<{
platform: string;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
isWindows11: boolean; hasCustomTitleBar: boolean;
}>; }>;
export const App = ({ export const App = ({
platform,
executeMenuRole, executeMenuRole,
isWindows11, hasCustomTitleBar,
}: AppPropsType): JSX.Element => { }: AppPropsType): JSX.Element => {
const i18n = useI18n(); const i18n = useI18n();
const theme = useTheme(); const theme = useTheme();
@ -32,8 +30,7 @@ export const App = ({
return ( return (
<TitleBarContainer <TitleBarContainer
iconSrc="../../images/icon_32.png" iconSrc="../../images/icon_32.png"
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >

View File

@ -3,12 +3,12 @@
.facade { .facade {
background: rgba(0, 0, 0, 0.33); background: rgba(0, 0, 0, 0.33);
width: 100vw; width: var(--window-width);
height: var(--window-height); height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: fixed; position: fixed;
left: 0; left: var(--window-border);
top: 0; top: var(--titlebar-height);
} }

View File

@ -18,8 +18,7 @@ const ColdRoot = () => (
<I18n messages={localeMessages} locale={SignalContext.config.locale}> <I18n messages={localeMessages} locale={SignalContext.config.locale}>
<App <App
executeMenuRole={SignalContext.executeMenuRole} executeMenuRole={SignalContext.executeMenuRole}
platform={SignalContext.OS.platform} hasCustomTitleBar={SignalContext.OS.hasCustomTitleBar()}
isWindows11={SignalContext.OS.isWindows11()}
/> />
</I18n> </I18n>
</Router> </Router>

View File

@ -672,11 +672,11 @@
@mixin install-screen { @mixin install-screen {
align-items: center; align-items: center;
display: flex; display: flex;
width: var(--window-width);
height: var(--window-height); height: var(--window-height);
justify-content: center; justify-content: center;
line-height: 30px; line-height: 30px;
user-select: none; user-select: none;
width: 100vw;
@include light-theme { @include light-theme {
background: $color-gray-02; background: $color-gray-02;

View File

@ -4167,12 +4167,12 @@ button.module-image__border-overlay:focus {
&__overlay { &__overlay {
display: flex; display: flex;
width: var(--window-width);
height: var(--window-height); height: var(--window-height);
justify-content: flex-end; justify-content: flex-end;
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
width: 100vw;
z-index: $z-index-popup; z-index: $z-index-popup;
} }
@ -5857,7 +5857,7 @@ button.module-image__border-overlay:focus {
position: fixed; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
width: 100vw; width: var(--window-width);
height: var(--window-height); height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -7457,25 +7457,25 @@ button.module-image__border-overlay:focus {
.module-modal-host__overlay { .module-modal-host__overlay {
background: $color-black-alpha-40; background: $color-black-alpha-40;
width: var(--window-width);
height: var(--window-height); height: var(--window-height);
left: 0; left: var(--window-border);
position: absolute; top: var(--titlebar-height);
top: 0; position: fixed;
width: 100vw;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
} }
.module-modal-host__overlay-container { .module-modal-host__overlay-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: var(--window-width);
height: var(--window-height); height: var(--window-height);
left: var(--window-border);
top: var(--titlebar-height);
justify-content: center; justify-content: center;
left: 0;
overflow: hidden; overflow: hidden;
padding: 20px; padding: 20px;
position: absolute; position: fixed;
top: 0;
width: 100vw;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
} }
@ -7612,9 +7612,9 @@ button.module-image__border-overlay:focus {
.module-progress-dialog__overlay { .module-progress-dialog__overlay {
background: $color-black-alpha-40; background: $color-black-alpha-40;
position: fixed; position: fixed;
left: 0; left: var(--window-border);
top: 0; top: var(--titlebar-height);
width: 100vw; width: var(--window-width);
height: var(--window-height); height: var(--window-height);
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -17,10 +17,20 @@ body {
} }
--window-height: 100vh; --window-height: 100vh;
--window-width: 100vw;
--unscaled-window-border: 0px;
--window-border: calc(var(--unscaled-window-border) / var(--zoom-factor));
--titlebar-height: 0px; --titlebar-height: 0px;
&.os-windows:not(.full-screen) { &.os-has-custom-titlebar:not(.full-screen) {
&:not(.maximized) {
--unscaled-window-border: 1px;
}
--titlebar-height: calc(28px / var(--zoom-factor)); --titlebar-height: calc(28px / var(--zoom-factor));
--window-height: calc(100vh - var(--titlebar-height)); --window-width: calc(100vw - 2 * var(--window-border));
--window-height: calc(
100vh - var(--titlebar-height) - 2 * var(--window-border)
);
} }
} }

View File

@ -7,12 +7,12 @@
background: $color-gray-95; background: $color-gray-95;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: var(--window-width);
height: var(--window-height); height: var(--window-height);
left: 0; left: 0;
position: absolute;
top: var(--titlebar-height); top: var(--titlebar-height);
position: absolute;
user-select: none; user-select: none;
width: 100vw;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
&__container { &__container {

View File

@ -19,7 +19,7 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
width: 380px; width: 380px;
padding-top: 42px; padding-top: calc(14px + var(--title-bar-drag-area-height));
&__header { &__header {
align-items: center; align-items: center;

View File

@ -11,12 +11,12 @@
background: $color-gray-95; background: $color-gray-95;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: var(--window-width);
height: 100vh; height: 100vh;
left: 0; left: 0;
position: absolute; position: absolute;
top: 0; top: 0;
user-select: none; user-select: none;
width: 100vw;
z-index: $z-index-popup-overlay; z-index: $z-index-popup-overlay;
&__container { &__container {

View File

@ -6,14 +6,41 @@
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
&__title { --border-color: transparent;
&--active {
--border-color: transparent;
}
border: var(--window-border) solid var(--border-color);
@mixin titlebar-position {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: calc(100vw * var(--zoom-factor)); width: calc(100vw * var(--zoom-factor));
z-index: $z-index-window-controls; z-index: $z-index-window-controls;
transform: scale(calc(1 / var(--zoom-factor))); transform: scale(calc(1 / var(--zoom-factor)));
transform-origin: 0 0; transform-origin: 0 0;
}
// Draw bottom-less border frame around titlebar to prevent border-bottom
// color from leaking to corners.
&:after {
content: '';
@include titlebar-position;
height: calc(var(--titlebar-height) * var(--zoom-factor));
border: var(--unscaled-window-border) solid var(--border-color);
border-bottom: none;
}
&__title {
@include titlebar-position;
border: var(--unscaled-window-border) solid transparent;
// This matches the inline styles of frameless-titlebar // This matches the inline styles of frameless-titlebar
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
@ -23,16 +50,13 @@
& button { & button {
font-family: inherit; font-family: inherit;
} }
// Shift titlebar down 1px on Windows 11 because otherwise window border
// will cover it.
&--extra-padding {
padding-top: 1px;
} }
&__padding {
height: calc(var(--titlebar-height) - var(--window-border));
} }
&__content { &__content {
margin-top: var(--titlebar-height);
height: var(--window-height); height: var(--window-height);
position: relative; position: relative;
} }

View File

@ -16,7 +16,7 @@ export const isWindows = (minVersion?: string): boolean => {
return is.undefined(minVersion) ? true : semver.gte(osRelease, minVersion); return is.undefined(minVersion) ? true : semver.gte(osRelease, minVersion);
}; };
export const isWindows11 = (): boolean => {
// See https://docs.microsoft.com/en-us/answers/questions/586619/windows-11-build-ver-is-still-10022000194.html // Windows 10 and above
return isWindows('10.0.22000'); export const hasCustomTitleBar = (): boolean =>
}; isWindows('10.0.0') || Boolean(process.env.CUSTOM_TITLEBAR);

View File

@ -1835,7 +1835,9 @@ export async function startApp(): Promise<void> {
window.reduxActions.app.openInstaller(); window.reduxActions.app.openInstaller();
} }
window.registerForActive(() => notificationService.clear()); const { activeWindowService } = window.SignalContext;
activeWindowService.registerForActive(() => notificationService.clear());
window.addEventListener('unload', () => notificationService.fastClear()); window.addEventListener('unload', () => notificationService.fastClear());
notificationService.on('click', (id, messageId) => { notificationService.on('click', (id, messageId) => {
@ -1848,7 +1850,7 @@ export async function startApp(): Promise<void> {
}); });
// Maybe refresh remote configuration when we become active // Maybe refresh remote configuration when we become active
window.registerForActive(async () => { activeWindowService.registerForActive(async () => {
strictAssert(server !== undefined, 'WebAPI not ready'); strictAssert(server !== undefined, 'WebAPI not ready');
try { try {

View File

@ -14,8 +14,7 @@ export type PropsType = {
environment: string; environment: string;
i18n: LocalizerType; i18n: LocalizerType;
version: string; version: string;
platform: string; hasCustomTitleBar: boolean;
isWindows11: boolean;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
}; };
@ -24,8 +23,7 @@ export const About = ({
i18n, i18n,
environment, environment,
version, version,
platform, hasCustomTitleBar,
isWindows11,
executeMenuRole, executeMenuRole,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
useEscapeHandling(closeAbout); useEscapeHandling(closeAbout);
@ -34,8 +32,7 @@ export const About = ({
return ( return (
<TitleBarContainer <TitleBarContainer
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >

View File

@ -36,8 +36,7 @@ type PropsType = {
isMaximized: boolean; isMaximized: boolean;
isFullScreen: boolean; isFullScreen: boolean;
menuOptions: MenuOptionsType; menuOptions: MenuOptionsType;
platform: string; hasCustomTitleBar: boolean;
isWindows11: boolean;
hideMenuBar: boolean; hideMenuBar: boolean;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
@ -59,11 +58,10 @@ export const App = ({
isFullScreen, isFullScreen,
isMaximized, isMaximized,
isShowingStoriesView, isShowingStoriesView,
isWindows11, hasCustomTitleBar,
localeMessages, localeMessages,
menuOptions, menuOptions,
openInbox, openInbox,
platform,
registerSingleDevice, registerSingleDevice,
renderCallManager, renderCallManager,
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
@ -152,8 +150,7 @@ export const App = ({
theme={theme} theme={theme}
isMaximized={isMaximized} isMaximized={isMaximized}
isFullScreen={isFullScreen} isFullScreen={isFullScreen}
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
titleBarDoubleClick={titleBarDoubleClick} titleBarDoubleClick={titleBarDoubleClick}
hasMenu hasMenu

View File

@ -26,8 +26,7 @@ const createProps = (): PropsType => ({
return 'https://picsum.photos/1800/900'; return 'https://picsum.photos/1800/900';
}, },
executeMenuRole: action('executeMenuRole'), executeMenuRole: action('executeMenuRole'),
platform: 'win32', hasCustomTitleBar: true,
isWindows11: false,
}); });
export default { export default {

View File

@ -31,8 +31,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
fetchLogs: () => Promise<string>; fetchLogs: () => Promise<string>;
uploadLogs: (logs: string) => Promise<string>; uploadLogs: (logs: string) => Promise<string>;
platform: string; hasCustomTitleBar: boolean;
isWindows11: boolean;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
}; };
@ -48,8 +47,7 @@ export const DebugLogWindow = ({
i18n, i18n,
fetchLogs, fetchLogs,
uploadLogs, uploadLogs,
platform, hasCustomTitleBar,
isWindows11,
executeMenuRole, executeMenuRole,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted); const [loadState, setLoadState] = useState<LoadState>(LoadState.NotStarted);
@ -147,8 +145,7 @@ export const DebugLogWindow = ({
return ( return (
<TitleBarContainer <TitleBarContainer
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >
@ -191,8 +188,7 @@ export const DebugLogWindow = ({
return ( return (
<TitleBarContainer <TitleBarContainer
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >

View File

@ -158,8 +158,7 @@ const createProps = (): PropsType => ({
i18n, i18n,
executeMenuRole: action('executeMenuRole'), executeMenuRole: action('executeMenuRole'),
platform: 'win32', hasCustomTitleBar: true,
isWindows11: false,
}); });
export default { export default {

View File

@ -102,8 +102,7 @@ export type PropsType = {
value: CustomColorType; value: CustomColorType;
} }
) => unknown; ) => unknown;
platform: string; hasCustomTitleBar: boolean;
isWindows11: boolean;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
// Limited support features // Limited support features
@ -230,7 +229,7 @@ export const Preferences = ({
isNotificationAttentionSupported, isNotificationAttentionSupported,
isSyncSupported, isSyncSupported,
isSystemTraySupported, isSystemTraySupported,
isWindows11, hasCustomTitleBar,
lastSyncTime, lastSyncTime,
makeSyncRequest, makeSyncRequest,
notificationContent, notificationContent,
@ -258,7 +257,6 @@ export const Preferences = ({
onThemeChange, onThemeChange,
onUniversalExpireTimerChange, onUniversalExpireTimerChange,
onZoomFactorChange, onZoomFactorChange,
platform,
removeCustomColor, removeCustomColor,
removeCustomColorOnConversations, removeCustomColorOnConversations,
resetAllChatColors, resetAllChatColors,
@ -1028,8 +1026,7 @@ export const Preferences = ({
return ( return (
<TitleBarContainer <TitleBarContainer
platform={platform} hasCustomTitleBar={hasCustomTitleBar}
isWindows11={isWindows11}
theme={theme} theme={theme}
executeMenuRole={executeMenuRole} executeMenuRole={executeMenuRole}
> >

View File

@ -12,6 +12,7 @@ import { createTemplate } from '../../app/menu';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import type { LocaleMessagesType } from '../types/I18N'; import type { LocaleMessagesType } from '../types/I18N';
import type { MenuOptionsType, MenuActionType } from '../types/menu'; import type { MenuOptionsType, MenuActionType } from '../types/menu';
import { useIsWindowActive } from '../hooks/useIsWindowActive';
export type MenuPropsType = Readonly<{ export type MenuPropsType = Readonly<{
hasMenu: true; hasMenu: true;
@ -28,9 +29,8 @@ export type PropsType = Readonly<{
theme: ThemeType; theme: ThemeType;
isMaximized?: boolean; isMaximized?: boolean;
isFullScreen?: boolean; isFullScreen?: boolean;
isWindows11: boolean; hasCustomTitleBar: boolean;
hideMenuBar?: boolean; hideMenuBar?: boolean;
platform: string;
executeMenuRole: ExecuteMenuRoleType; executeMenuRole: ExecuteMenuRoleType;
titleBarDoubleClick?: () => void; titleBarDoubleClick?: () => void;
children: ReactNode; children: ReactNode;
@ -116,16 +116,17 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
theme, theme,
isMaximized, isMaximized,
isFullScreen, isFullScreen,
isWindows11, hasCustomTitleBar,
hideMenuBar, hideMenuBar,
executeMenuRole, executeMenuRole,
titleBarDoubleClick, titleBarDoubleClick,
children, children,
hasMenu, hasMenu,
platform,
iconSrc = 'images/icon_32.png', iconSrc = 'images/icon_32.png',
} = props; } = props;
const isWindowActive = useIsWindowActive();
const titleBarTheme = useMemo( const titleBarTheme = useMemo(
() => ({ () => ({
bar: { bar: {
@ -201,7 +202,7 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
[theme, hideMenuBar] [theme, hideMenuBar]
); );
if (platform !== 'win32' || isFullScreen) { if (!hasCustomTitleBar || isFullScreen) {
return <>{children}</>; return <>{children}</>;
} }
@ -236,17 +237,18 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
} }
return ( return (
<div className="TitleBarContainer"> <div
<TitleBar
className={classNames( className={classNames(
'TitleBarContainer__title', 'TitleBarContainer',
isWindowActive ? 'TitleBarContainer--active' : null
// Add a pixel of padding on non-maximized Windows 11 titlebar.
isWindows11 && !isMaximized
? 'TitleBarContainer__title--extra-padding'
: null
)} )}
platform={platform} >
<div className="TitleBarContainer__padding" />
<div className="TitleBarContainer__content">{children}</div>
<TitleBar
className="TitleBarContainer__title"
platform="win32"
iconSrc={iconSrc} iconSrc={iconSrc}
theme={titleBarTheme} theme={titleBarTheme}
maximized={isMaximized} maximized={isMaximized}
@ -254,8 +256,6 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => {
onDoubleClick={titleBarDoubleClick} onDoubleClick={titleBarDoubleClick}
hideControls hideControls
/> />
<div className="TitleBarContainer__content">{children}</div>
</div> </div>
); );
}; };

View File

@ -36,11 +36,6 @@ export default {
// eslint-disable-next-line // eslint-disable-next-line
const noop = () => {}; const noop = () => {};
Object.assign(window, {
registerForActive: noop,
unregisterForActive: noop,
});
const items: Record<string, TimelineItemType> = { const items: Record<string, TimelineItemType> = {
'id-1': { 'id-1': {
type: 'message', type: 'message',

View File

@ -573,7 +573,9 @@ export class Timeline extends React.Component<
this.updateIntersectionObserver(); this.updateIntersectionObserver();
window.registerForActive(this.markNewestBottomVisibleMessageRead); window.SignalContext.activeWindowService.registerForActive(
this.markNewestBottomVisibleMessageRead
);
this.delayedPeekTimeout = setTimeout(() => { this.delayedPeekTimeout = setTimeout(() => {
const { id, peekGroupCallForTheFirstTime } = this.props; const { id, peekGroupCallForTheFirstTime } = this.props;
@ -590,7 +592,9 @@ export class Timeline extends React.Component<
public override componentWillUnmount(): void { public override componentWillUnmount(): void {
const { delayedPeekTimeout, peekInterval } = this; const { delayedPeekTimeout, peekInterval } = this;
window.unregisterForActive(this.markNewestBottomVisibleMessageRead); window.SignalContext.activeWindowService.unregisterForActive(
this.markNewestBottomVisibleMessageRead
);
this.intersectionObserver?.disconnect(); this.intersectionObserver?.disconnect();

View File

@ -0,0 +1,23 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useEffect, useState } from 'react';
export function useIsWindowActive(): boolean {
const { activeWindowService } = window.SignalContext;
const [isActive, setIsActive] = useState(activeWindowService.isActive());
useEffect(() => {
const update = (newIsActive: boolean): void => {
setIsActive(newIsActive);
};
activeWindowService.registerForChange(update);
return () => {
activeWindowService.unregisterForChange(update);
};
}, [activeWindowService]);
return isActive;
}

View File

@ -1,10 +1,14 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
{ {
const updateFullScreenClass = (isFullScreen: boolean) => { const updateFullScreenClass = (
isFullScreen: boolean,
isMaximized: boolean
) => {
document.body.classList.toggle('full-screen', isFullScreen); document.body.classList.toggle('full-screen', isFullScreen);
document.body.classList.toggle('maximized', isMaximized);
}; };
updateFullScreenClass(window.isFullScreen()); updateFullScreenClass(window.isFullScreen(), window.isMaximized());
window.onFullScreenChange = updateFullScreenClass; window.onFullScreenChange = updateFullScreenClass;
} }

View File

@ -1417,7 +1417,7 @@ export class ConversationModel extends window.Backbone
messagesAdded({ messagesAdded({
conversationId, conversationId,
messages: [{ ...message.attributes }], messages: [{ ...message.attributes }],
isActive: window.isActive(), isActive: window.SignalContext.activeWindowService.isActive(),
isJustSent, isJustSent,
isNewMessage: true, isNewMessage: true,
}); });
@ -1567,7 +1567,7 @@ export class ConversationModel extends window.Backbone
messages: cleaned.map((messageModel: MessageModel) => ({ messages: cleaned.map((messageModel: MessageModel) => ({
...messageModel.attributes, ...messageModel.attributes,
})), })),
isActive: window.isActive(), isActive: window.SignalContext.activeWindowService.isActive(),
isJustSent: false, isJustSent: false,
isNewMessage: false, isNewMessage: false,
}); });
@ -1620,7 +1620,7 @@ export class ConversationModel extends window.Backbone
messages: cleaned.map((messageModel: MessageModel) => ({ messages: cleaned.map((messageModel: MessageModel) => ({
...messageModel.attributes, ...messageModel.attributes,
})), })),
isActive: window.isActive(), isActive: window.SignalContext.activeWindowService.isActive(),
isJustSent: false, isJustSent: false,
isNewMessage: false, isNewMessage: false,
}); });

View File

@ -25,6 +25,8 @@ export class ActiveWindowService {
private activeCallbacks: Array<() => void> = []; private activeCallbacks: Array<() => void> = [];
private changeCallbacks: Array<(isActive: boolean) => void> = [];
private lastActiveEventAt = -Infinity; private lastActiveEventAt = -Infinity;
private callActiveCallbacks: () => void; private callActiveCallbacks: () => void;
@ -73,6 +75,16 @@ export class ActiveWindowService {
); );
} }
registerForChange(callback: (isActive: boolean) => void): void {
this.changeCallbacks.push(callback);
}
unregisterForChange(callback: (isActive: boolean) => void): void {
this.changeCallbacks = this.changeCallbacks.filter(
item => item !== callback
);
}
private onActiveEvent(): void { private onActiveEvent(): void {
this.updateState(() => { this.updateState(() => {
this.lastActiveEventAt = Date.now(); this.lastActiveEventAt = Date.now();
@ -93,5 +105,11 @@ export class ActiveWindowService {
if (!wasActiveBefore && isActiveNow) { if (!wasActiveBefore && isActiveNow) {
this.callActiveCallbacks(); this.callActiveCallbacks();
} }
if (wasActiveBefore !== isActiveNow) {
for (const callback of this.changeCallbacks) {
callback(isActiveNow);
}
}
} }
} }

View File

@ -228,7 +228,7 @@ class NotificationService extends EventEmitter {
} }
const { notificationData } = this; const { notificationData } = this;
const isAppFocused = window.isActive(); const isAppFocused = window.SignalContext.activeWindowService.isActive();
const userSetting = this.getNotificationSetting(); const userSetting = this.getNotificationSetting();
// This isn't a boolean because TypeScript isn't smart enough to know that, if // This isn't a boolean because TypeScript isn't smart enough to know that, if

View File

@ -14,4 +14,8 @@
} }
document.body.classList.add(className); document.body.classList.add(className);
if (window.SignalContext.OS.hasCustomTitleBar()) {
document.body.classList.add('os-has-custom-titlebar');
}
} }

View File

@ -22,7 +22,6 @@ import {
getIsMainWindowMaximized, getIsMainWindowMaximized,
getIsMainWindowFullScreen, getIsMainWindowFullScreen,
getMenuOptions, getMenuOptions,
getPlatform,
} from '../selectors/user'; } from '../selectors/user';
import { shouldShowStoriesView } from '../selectors/stories'; import { shouldShowStoriesView } from '../selectors/stories';
import { getHideMenuBar } from '../selectors/items'; import { getHideMenuBar } from '../selectors/items';
@ -42,8 +41,7 @@ const mapStateToProps = (state: StateType) => {
isMaximized: getIsMainWindowMaximized(state), isMaximized: getIsMainWindowMaximized(state),
isFullScreen: getIsMainWindowFullScreen(state), isFullScreen: getIsMainWindowFullScreen(state),
menuOptions: getMenuOptions(state), menuOptions: getMenuOptions(state),
platform: getPlatform(state), hasCustomTitleBar: window.SignalContext.OS.hasCustomTitleBar(),
isWindows11: window.SignalContext.OS.isWindows11(),
hideMenuBar: getHideMenuBar(state), hideMenuBar: getHideMenuBar(state),
renderCallManager: () => <SmartCallManager />, renderCallManager: () => <SmartCallManager />,
renderCustomizingPreferredReactionsModal: () => ( renderCustomizingPreferredReactionsModal: () => (

View File

@ -53,7 +53,8 @@ async function notifyForCall(
isVideoCall: boolean isVideoCall: boolean
): Promise<void> { ): Promise<void> {
const shouldNotify = const shouldNotify =
!window.isActive() && window.Events.getCallSystemNotification(); !window.SignalContext.activeWindowService.isActive() &&
window.Events.getCallSystemNotification();
if (!shouldNotify) { if (!shouldNotify) {
return; return;
} }

View File

@ -92,6 +92,7 @@ export const rendererConfigSchema = z.object({
// Only used by main window // Only used by main window
isMainWindowFullScreen: z.boolean(), isMainWindowFullScreen: z.boolean(),
isMainWindowMaximized: z.boolean(),
// Only for tests // Only for tests
argv: configOptionalStringSchema, argv: configOptionalStringSchema,

View File

@ -450,7 +450,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}; };
const markMessageRead = async (messageId: string) => { const markMessageRead = async (messageId: string) => {
if (!window.isActive()) { if (!window.SignalContext.activeWindowService.isActive()) {
return; return;
} }

6
ts/window.d.ts vendored
View File

@ -291,10 +291,10 @@ declare global {
waitForEmptyEventQueue: () => Promise<void>; waitForEmptyEventQueue: () => Promise<void>;
getVersion: () => string; getVersion: () => string;
i18n: LocalizerType; i18n: LocalizerType;
isActive: () => boolean;
isAfterVersion: (version: string, anotherVersion: string) => boolean; isAfterVersion: (version: string, anotherVersion: string) => boolean;
isBeforeVersion: (version: string, anotherVersion: string) => boolean; isBeforeVersion: (version: string, anotherVersion: string) => boolean;
isFullScreen: () => boolean; isFullScreen: () => boolean;
isMaximized: () => boolean;
initialTheme?: ThemeType; initialTheme?: ThemeType;
libphonenumberInstance: { libphonenumberInstance: {
parse: (number: string) => PhoneNumber; parse: (number: string) => PhoneNumber;
@ -303,12 +303,11 @@ declare global {
}; };
libphonenumberFormat: typeof PhoneNumberFormat; libphonenumberFormat: typeof PhoneNumberFormat;
nodeSetImmediate: typeof setImmediate; nodeSetImmediate: typeof setImmediate;
onFullScreenChange: (fullScreen: boolean) => void; onFullScreenChange: (fullScreen: boolean, maximized: boolean) => void;
platform: string; platform: string;
preloadedImages: Array<WhatIsThis>; preloadedImages: Array<WhatIsThis>;
reduxActions: ReduxActions; reduxActions: ReduxActions;
reduxStore: Store<StateType>; reduxStore: Store<StateType>;
registerForActive: (handler: () => void) => void;
restart: () => void; restart: () => void;
setImmediate: typeof setImmediate; setImmediate: typeof setImmediate;
showWindow: () => void; showWindow: () => void;
@ -326,7 +325,6 @@ declare global {
systemTheme: WhatIsThis; systemTheme: WhatIsThis;
textsecure: typeof textsecure; textsecure: typeof textsecure;
titleBarDoubleClick: () => void; titleBarDoubleClick: () => void;
unregisterForActive: (handler: () => void) => void;
updateTrayIcon: (count: number) => void; updateTrayIcon: (count: number) => void;
Backbone: typeof Backbone; Backbone: typeof Backbone;
CI?: CI; CI?: CI;

View File

@ -36,8 +36,7 @@ contextBridge.exposeInMainWorld('SignalContext', {
environment: `${environmentText.join(' - ')}${platform}`, environment: `${environmentText.join(' - ')}${platform}`,
i18n: SignalContext.i18n, i18n: SignalContext.i18n,
version: SignalContext.getVersion(), version: SignalContext.getVersion(),
platform: process.platform, hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(),
isWindows11: SignalContext.OS.isWindows11(),
executeMenuRole: SignalContext.executeMenuRole, executeMenuRole: SignalContext.executeMenuRole,
}), }),
document.getElementById('app') document.getElementById('app')

View File

@ -12,6 +12,7 @@ import type { LocaleMessagesType } from '../types/I18N';
import type { NativeThemeType } from '../context/createNativeThemeListener'; import type { NativeThemeType } from '../context/createNativeThemeListener';
import type { SettingType } from '../util/preload'; import type { SettingType } from '../util/preload';
import type { RendererConfigType } from '../types/RendererConfig'; import type { RendererConfigType } from '../types/RendererConfig';
import { ActiveWindowService } from '../services/ActiveWindowService';
import { Bytes } from '../context/Bytes'; import { Bytes } from '../context/Bytes';
import { Crypto } from '../context/Crypto'; import { Crypto } from '../context/Crypto';
@ -28,7 +29,10 @@ import { createSetting } from '../util/preload';
import { initialize as initializeLogging } from '../logging/set_up_renderer_logging'; import { initialize as initializeLogging } from '../logging/set_up_renderer_logging';
import { waitForSettingsChange } from './waitForSettingsChange'; import { waitForSettingsChange } from './waitForSettingsChange';
import { createNativeThemeListener } from '../context/createNativeThemeListener'; import { createNativeThemeListener } from '../context/createNativeThemeListener';
import { isWindows, isWindows11, isLinux, isMacOS } from '../OS'; import { isWindows, isLinux, isMacOS, hasCustomTitleBar } from '../OS';
const activeWindowService = new ActiveWindowService();
activeWindowService.initialize(window.document, ipcRenderer);
const params = new URLSearchParams(document.location.search); const params = new URLSearchParams(document.location.search);
const configParam = params.get('config'); const configParam = params.get('config');
@ -58,6 +62,7 @@ export type SignalContextType = {
nativeThemeListener: NativeThemeType; nativeThemeListener: NativeThemeType;
setIsCallActive: (isCallActive: boolean) => unknown; setIsCallActive: (isCallActive: boolean) => unknown;
activeWindowService: typeof activeWindowService;
Settings: { Settings: {
themeSetting: SettingType<IPCEventsValuesType['themeSetting']>; themeSetting: SettingType<IPCEventsValuesType['themeSetting']>;
waitForChange: () => Promise<void>; waitForChange: () => Promise<void>;
@ -65,9 +70,9 @@ export type SignalContextType = {
OS: { OS: {
platform: string; platform: string;
isWindows: typeof isWindows; isWindows: typeof isWindows;
isWindows11: typeof isWindows11;
isLinux: typeof isLinux; isLinux: typeof isLinux;
isMacOS: typeof isMacOS; isMacOS: typeof isMacOS;
hasCustomTitleBar: typeof hasCustomTitleBar;
}; };
config: RendererConfigType; config: RendererConfigType;
getAppInstance: () => string | undefined; getAppInstance: () => string | undefined;
@ -86,6 +91,7 @@ export type SignalContextType = {
}; };
export const SignalContext: SignalContextType = { export const SignalContext: SignalContextType = {
activeWindowService,
Settings: { Settings: {
themeSetting: createSetting('themeSetting', { setter: false }), themeSetting: createSetting('themeSetting', { setter: false }),
waitForChange: waitForSettingsChange, waitForChange: waitForSettingsChange,
@ -93,9 +99,9 @@ export const SignalContext: SignalContextType = {
OS: { OS: {
platform: process.platform, platform: process.platform,
isWindows, isWindows,
isWindows11,
isLinux, isLinux,
isMacOS, isMacOS,
hasCustomTitleBar,
}, },
bytes: new Bytes(), bytes: new Bytes(),
config, config,

View File

@ -26,8 +26,7 @@ contextBridge.exposeInMainWorld('SignalContext', {
ReactDOM.render( ReactDOM.render(
React.createElement(DebugLogWindow, { React.createElement(DebugLogWindow, {
platform: process.platform, hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(),
isWindows11: SignalContext.OS.isWindows11(),
executeMenuRole: SignalContext.executeMenuRole, executeMenuRole: SignalContext.executeMenuRole,
closeWindow: () => SignalContext.executeMenuRole('close'), closeWindow: () => SignalContext.executeMenuRole('close'),
downloadLog: (logText: string) => downloadLog: (logText: string) =>

View File

@ -254,14 +254,17 @@ window.sendChallengeRequest = request => ipc.send('challenge:request', request);
{ {
let isFullScreen = Boolean(config.isMainWindowFullScreen); let isFullScreen = Boolean(config.isMainWindowFullScreen);
let isMaximized = Boolean(config.isMainWindowMaximized);
window.isFullScreen = () => isFullScreen; window.isFullScreen = () => isFullScreen;
window.isMaximized = () => isMaximized;
// This is later overwritten. // This is later overwritten.
window.onFullScreenChange = noop; window.onFullScreenChange = noop;
ipc.on('full-screen-change', (_event, isFull) => { ipc.on('window:set-window-stats', (_event, stats) => {
isFullScreen = Boolean(isFull); isFullScreen = Boolean(stats.isFullScreen);
window.onFullScreenChange(isFullScreen); isMaximized = Boolean(stats.isMaximized);
window.onFullScreenChange(isFullScreen, isMaximized);
}); });
} }

View File

@ -1,7 +1,6 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer as ipc } from 'electron';
import Backbone from 'backbone'; import Backbone from 'backbone';
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'; import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber';
import * as React from 'react'; import * as React from 'react';
@ -12,7 +11,6 @@ import PQueue from 'p-queue';
import { textsecure } from '../../textsecure'; import { textsecure } from '../../textsecure';
import { imageToBlurHash } from '../../util/imageToBlurHash'; import { imageToBlurHash } from '../../util/imageToBlurHash';
import { ActiveWindowService } from '../../services/ActiveWindowService';
import * as Attachments from '../attachments'; import * as Attachments from '../attachments';
import { setup } from '../../signal'; import { setup } from '../../signal';
import { addSensitivePath } from '../../util/privacy'; import { addSensitivePath } from '../../util/privacy';
@ -44,14 +42,6 @@ window.imageToBlurHash = imageToBlurHash;
window.libphonenumberInstance = PhoneNumberUtil.getInstance(); window.libphonenumberInstance = PhoneNumberUtil.getInstance();
window.libphonenumberFormat = PhoneNumberFormat; window.libphonenumberFormat = PhoneNumberFormat;
const activeWindowService = new ActiveWindowService();
activeWindowService.initialize(window.document, ipc);
window.isActive = activeWindowService.isActive.bind(activeWindowService);
window.registerForActive =
activeWindowService.registerForActive.bind(activeWindowService);
window.unregisterForActive =
activeWindowService.unregisterForActive.bind(activeWindowService);
window.React = React; window.React = React;
window.ReactDOM = ReactDOM; window.ReactDOM = ReactDOM;
window.PQueue = PQueue; window.PQueue = PQueue;

View File

@ -341,8 +341,7 @@ const renderPreferences = async () => {
i18n: SignalContext.i18n, i18n: SignalContext.i18n,
platform: process.platform, hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(),
isWindows11: SignalContext.OS.isWindows11(),
executeMenuRole: SignalContext.executeMenuRole, executeMenuRole: SignalContext.executeMenuRole,
}; };