From f92be05b15cb4c206e0ceda8a7fb4fdd11a547b9 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:44:53 -0700 Subject: [PATCH] Titlebar fixes --- .storybook/preview-head.html | 19 ++++++++-- app/config.ts | 1 + app/main.ts | 28 +++++++------- sticker-creator/app/index.tsx | 9 ++--- sticker-creator/components/ConfirmModal.scss | 6 +-- sticker-creator/root.tsx | 3 +- stylesheets/_mixins.scss | 2 +- stylesheets/_modules.scss | 26 ++++++------- stylesheets/_titlebar.scss | 14 ++++++- stylesheets/components/MediaEditor.scss | 4 +- stylesheets/components/Stories.scss | 2 +- stylesheets/components/StoryCreator.scss | 2 +- stylesheets/components/TitleBarContainer.scss | 38 +++++++++++++++---- ts/OS.ts | 8 ++-- ts/background.ts | 6 ++- ts/components/About.tsx | 9 ++--- ts/components/App.tsx | 9 ++--- ts/components/DebugLogWindow.stories.tsx | 3 +- ts/components/DebugLogWindow.tsx | 12 ++---- ts/components/Preferences.stories.tsx | 3 +- ts/components/Preferences.tsx | 9 ++--- ts/components/TitleBarContainer.tsx | 34 ++++++++--------- .../conversation/Timeline.stories.tsx | 5 --- ts/components/conversation/Timeline.tsx | 8 +++- ts/hooks/useIsWindowActive.ts | 23 +++++++++++ ts/manage_full_screen_class.ts | 10 +++-- ts/models/conversations.ts | 6 +-- ts/services/ActiveWindowService.ts | 18 +++++++++ ts/services/notifications.ts | 2 +- ts/set_os_class.ts | 4 ++ ts/state/smart/App.tsx | 4 +- ts/state/smart/CallManager.tsx | 3 +- ts/types/RendererConfig.ts | 1 + ts/views/conversation_view.tsx | 2 +- ts/window.d.ts | 6 +-- ts/windows/about/preload.ts | 3 +- ts/windows/context.ts | 12 ++++-- ts/windows/debuglog/preload.ts | 3 +- ts/windows/main/phase1-ipc.ts | 9 +++-- ts/windows/main/phase2-dependencies.ts | 10 ----- ts/windows/settings/preload.ts | 3 +- 41 files changed, 225 insertions(+), 154 deletions(-) create mode 100644 ts/hooks/useIsWindowActive.ts diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 912fcdae2..98c5172db 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -9,6 +9,9 @@ type="text/css" /> diff --git a/app/config.ts b/app/config.ts index 7991a2d78..7a4bb3c04 100644 --- a/app/config.ts +++ b/app/config.ts @@ -34,6 +34,7 @@ if (getEnvironment() === Environment.Production) { process.env.SUPPRESS_NO_CONFIG_WARNING = ''; process.env.NODE_TLS_REJECT_UNAUTHORIZED = ''; process.env.SIGNAL_ENABLE_HTTP = ''; + process.env.CUSTOM_TITLEBAR = ''; } // We load config after we've made our modifications to NODE_ENV diff --git a/app/main.ts b/app/main.ts index 738de01c7..544c93869 100644 --- a/app/main.ts +++ b/app/main.ts @@ -432,6 +432,7 @@ async function prepareUrl( // Only used by the main window isMainWindowFullScreen: Boolean(mainWindow?.isFullScreen()), + isMainWindowMaximized: Boolean(mainWindow?.isMaximized()), // Only for tests argv: JSON.stringify(process.argv), @@ -499,6 +500,17 @@ function handleCommonWindowEvents( activeWindows.add(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` let lastZoomFactor = window.webContents.getZoomFactor(); const onZoomChanged = () => { @@ -600,12 +612,12 @@ const mainTitleBarStyle = ? ('default' as const) : ('hidden' as const); -const nonMainTitleBarStyle = OS.isWindows() +const nonMainTitleBarStyle = OS.hasCustomTitleBar() ? ('hidden' as const) : ('default' as const); async function getTitleBarOverlay(): Promise { - if (!OS.isWindows()) { + if (!OS.hasCustomTitleBar()) { return false; } @@ -782,18 +794,6 @@ async function createWindow() { mainWindow.on('resize', 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) { mainWindow.loadURL(await prepareFileUrl([__dirname, '../test/index.html'])); } else { diff --git a/sticker-creator/app/index.tsx b/sticker-creator/app/index.tsx index 68c6b503f..dbbb16cb4 100644 --- a/sticker-creator/app/index.tsx +++ b/sticker-creator/app/index.tsx @@ -16,15 +16,13 @@ import type { ExecuteMenuRoleType } from '../../ts/components/TitleBarContainer' import { useTheme } from '../../ts/hooks/useTheme'; export type AppPropsType = Readonly<{ - platform: string; executeMenuRole: ExecuteMenuRoleType; - isWindows11: boolean; + hasCustomTitleBar: boolean; }>; export const App = ({ - platform, executeMenuRole, - isWindows11, + hasCustomTitleBar, }: AppPropsType): JSX.Element => { const i18n = useI18n(); const theme = useTheme(); @@ -32,8 +30,7 @@ export const App = ({ return ( diff --git a/sticker-creator/components/ConfirmModal.scss b/sticker-creator/components/ConfirmModal.scss index 0c220f61d..89d1bbdb1 100644 --- a/sticker-creator/components/ConfirmModal.scss +++ b/sticker-creator/components/ConfirmModal.scss @@ -3,12 +3,12 @@ .facade { background: rgba(0, 0, 0, 0.33); - width: 100vw; + width: var(--window-width); height: var(--window-height); display: flex; justify-content: center; align-items: center; position: fixed; - left: 0; - top: 0; + left: var(--window-border); + top: var(--titlebar-height); } diff --git a/sticker-creator/root.tsx b/sticker-creator/root.tsx index b17489ac0..fb4714d92 100644 --- a/sticker-creator/root.tsx +++ b/sticker-creator/root.tsx @@ -18,8 +18,7 @@ const ColdRoot = () => ( diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss index 36db94624..627e487c4 100644 --- a/stylesheets/_mixins.scss +++ b/stylesheets/_mixins.scss @@ -672,11 +672,11 @@ @mixin install-screen { align-items: center; display: flex; + width: var(--window-width); height: var(--window-height); justify-content: center; line-height: 30px; user-select: none; - width: 100vw; @include light-theme { background: $color-gray-02; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 9412641a6..755881742 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -4167,12 +4167,12 @@ button.module-image__border-overlay:focus { &__overlay { display: flex; + width: var(--window-width); height: var(--window-height); justify-content: flex-end; left: 0; position: absolute; top: 0; - width: 100vw; z-index: $z-index-popup; } @@ -5857,7 +5857,7 @@ button.module-image__border-overlay:focus { position: fixed; left: 0; top: 0; - width: 100vw; + width: var(--window-width); height: var(--window-height); display: flex; justify-content: center; @@ -7457,25 +7457,25 @@ button.module-image__border-overlay:focus { .module-modal-host__overlay { background: $color-black-alpha-40; + width: var(--window-width); height: var(--window-height); - left: 0; - position: absolute; - top: 0; - width: 100vw; + left: var(--window-border); + top: var(--titlebar-height); + position: fixed; z-index: $z-index-popup-overlay; } .module-modal-host__overlay-container { display: flex; flex-direction: column; + width: var(--window-width); height: var(--window-height); + left: var(--window-border); + top: var(--titlebar-height); justify-content: center; - left: 0; overflow: hidden; padding: 20px; - position: absolute; - top: 0; - width: 100vw; + position: fixed; z-index: $z-index-popup-overlay; } @@ -7612,9 +7612,9 @@ button.module-image__border-overlay:focus { .module-progress-dialog__overlay { background: $color-black-alpha-40; position: fixed; - left: 0; - top: 0; - width: 100vw; + left: var(--window-border); + top: var(--titlebar-height); + width: var(--window-width); height: var(--window-height); display: flex; justify-content: center; diff --git a/stylesheets/_titlebar.scss b/stylesheets/_titlebar.scss index f0d879eb2..83056fb88 100644 --- a/stylesheets/_titlebar.scss +++ b/stylesheets/_titlebar.scss @@ -17,10 +17,20 @@ body { } --window-height: 100vh; + --window-width: 100vw; + --unscaled-window-border: 0px; + --window-border: calc(var(--unscaled-window-border) / var(--zoom-factor)); --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)); - --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) + ); } } diff --git a/stylesheets/components/MediaEditor.scss b/stylesheets/components/MediaEditor.scss index 6289611f5..79d0eb1e6 100644 --- a/stylesheets/components/MediaEditor.scss +++ b/stylesheets/components/MediaEditor.scss @@ -7,12 +7,12 @@ background: $color-gray-95; display: flex; flex-direction: column; + width: var(--window-width); height: var(--window-height); left: 0; - position: absolute; top: var(--titlebar-height); + position: absolute; user-select: none; - width: 100vw; z-index: $z-index-popup-overlay; &__container { diff --git a/stylesheets/components/Stories.scss b/stylesheets/components/Stories.scss index 649ca5295..7b015986c 100644 --- a/stylesheets/components/Stories.scss +++ b/stylesheets/components/Stories.scss @@ -19,7 +19,7 @@ flex-direction: column; height: 100%; width: 380px; - padding-top: 42px; + padding-top: calc(14px + var(--title-bar-drag-area-height)); &__header { align-items: center; diff --git a/stylesheets/components/StoryCreator.scss b/stylesheets/components/StoryCreator.scss index ad6b7f530..faeda4800 100644 --- a/stylesheets/components/StoryCreator.scss +++ b/stylesheets/components/StoryCreator.scss @@ -11,12 +11,12 @@ background: $color-gray-95; display: flex; flex-direction: column; + width: var(--window-width); height: 100vh; left: 0; position: absolute; top: 0; user-select: none; - width: 100vw; z-index: $z-index-popup-overlay; &__container { diff --git a/stylesheets/components/TitleBarContainer.scss b/stylesheets/components/TitleBarContainer.scss index 316c832a4..e6f1c1658 100644 --- a/stylesheets/components/TitleBarContainer.scss +++ b/stylesheets/components/TitleBarContainer.scss @@ -6,14 +6,41 @@ flex-direction: column; height: 100vh; - &__title { + --border-color: transparent; + + &--active { + --border-color: transparent; + } + + border: var(--window-border) solid var(--border-color); + + @mixin titlebar-position { position: fixed; top: 0; left: 0; + width: calc(100vw * var(--zoom-factor)); z-index: $z-index-window-controls; transform: scale(calc(1 / var(--zoom-factor))); 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 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, @@ -23,16 +50,13 @@ & button { 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 { - margin-top: var(--titlebar-height); height: var(--window-height); position: relative; } diff --git a/ts/OS.ts b/ts/OS.ts index 1049f852e..0f19966a8 100644 --- a/ts/OS.ts +++ b/ts/OS.ts @@ -16,7 +16,7 @@ export const isWindows = (minVersion?: string): boolean => { 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 - return isWindows('10.0.22000'); -}; + +// Windows 10 and above +export const hasCustomTitleBar = (): boolean => + isWindows('10.0.0') || Boolean(process.env.CUSTOM_TITLEBAR); diff --git a/ts/background.ts b/ts/background.ts index b6846f5cb..d1792d26f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1835,7 +1835,9 @@ export async function startApp(): Promise { window.reduxActions.app.openInstaller(); } - window.registerForActive(() => notificationService.clear()); + const { activeWindowService } = window.SignalContext; + + activeWindowService.registerForActive(() => notificationService.clear()); window.addEventListener('unload', () => notificationService.fastClear()); notificationService.on('click', (id, messageId) => { @@ -1848,7 +1850,7 @@ export async function startApp(): Promise { }); // Maybe refresh remote configuration when we become active - window.registerForActive(async () => { + activeWindowService.registerForActive(async () => { strictAssert(server !== undefined, 'WebAPI not ready'); try { diff --git a/ts/components/About.tsx b/ts/components/About.tsx index f79b02861..d1c559f0f 100644 --- a/ts/components/About.tsx +++ b/ts/components/About.tsx @@ -14,8 +14,7 @@ export type PropsType = { environment: string; i18n: LocalizerType; version: string; - platform: string; - isWindows11: boolean; + hasCustomTitleBar: boolean; executeMenuRole: ExecuteMenuRoleType; }; @@ -24,8 +23,7 @@ export const About = ({ i18n, environment, version, - platform, - isWindows11, + hasCustomTitleBar, executeMenuRole, }: PropsType): JSX.Element => { useEscapeHandling(closeAbout); @@ -34,8 +32,7 @@ export const About = ({ return ( diff --git a/ts/components/App.tsx b/ts/components/App.tsx index 0aaf185a8..cda1e1a74 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -36,8 +36,7 @@ type PropsType = { isMaximized: boolean; isFullScreen: boolean; menuOptions: MenuOptionsType; - platform: string; - isWindows11: boolean; + hasCustomTitleBar: boolean; hideMenuBar: boolean; executeMenuRole: ExecuteMenuRoleType; @@ -59,11 +58,10 @@ export const App = ({ isFullScreen, isMaximized, isShowingStoriesView, - isWindows11, + hasCustomTitleBar, localeMessages, menuOptions, openInbox, - platform, registerSingleDevice, renderCallManager, renderCustomizingPreferredReactionsModal, @@ -152,8 +150,7 @@ export const App = ({ theme={theme} isMaximized={isMaximized} isFullScreen={isFullScreen} - platform={platform} - isWindows11={isWindows11} + hasCustomTitleBar={hasCustomTitleBar} executeMenuRole={executeMenuRole} titleBarDoubleClick={titleBarDoubleClick} hasMenu diff --git a/ts/components/DebugLogWindow.stories.tsx b/ts/components/DebugLogWindow.stories.tsx index 7a424ed4f..7118a5bbe 100644 --- a/ts/components/DebugLogWindow.stories.tsx +++ b/ts/components/DebugLogWindow.stories.tsx @@ -26,8 +26,7 @@ const createProps = (): PropsType => ({ return 'https://picsum.photos/1800/900'; }, executeMenuRole: action('executeMenuRole'), - platform: 'win32', - isWindows11: false, + hasCustomTitleBar: true, }); export default { diff --git a/ts/components/DebugLogWindow.tsx b/ts/components/DebugLogWindow.tsx index ece3b652b..26bf221e4 100644 --- a/ts/components/DebugLogWindow.tsx +++ b/ts/components/DebugLogWindow.tsx @@ -31,8 +31,7 @@ export type PropsType = { i18n: LocalizerType; fetchLogs: () => Promise; uploadLogs: (logs: string) => Promise; - platform: string; - isWindows11: boolean; + hasCustomTitleBar: boolean; executeMenuRole: ExecuteMenuRoleType; }; @@ -48,8 +47,7 @@ export const DebugLogWindow = ({ i18n, fetchLogs, uploadLogs, - platform, - isWindows11, + hasCustomTitleBar, executeMenuRole, }: PropsType): JSX.Element => { const [loadState, setLoadState] = useState(LoadState.NotStarted); @@ -147,8 +145,7 @@ export const DebugLogWindow = ({ return ( @@ -191,8 +188,7 @@ export const DebugLogWindow = ({ return ( diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index 58290e641..9263132a1 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -158,8 +158,7 @@ const createProps = (): PropsType => ({ i18n, executeMenuRole: action('executeMenuRole'), - platform: 'win32', - isWindows11: false, + hasCustomTitleBar: true, }); export default { diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index ce6463b2e..5a426323e 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -102,8 +102,7 @@ export type PropsType = { value: CustomColorType; } ) => unknown; - platform: string; - isWindows11: boolean; + hasCustomTitleBar: boolean; executeMenuRole: ExecuteMenuRoleType; // Limited support features @@ -230,7 +229,7 @@ export const Preferences = ({ isNotificationAttentionSupported, isSyncSupported, isSystemTraySupported, - isWindows11, + hasCustomTitleBar, lastSyncTime, makeSyncRequest, notificationContent, @@ -258,7 +257,6 @@ export const Preferences = ({ onThemeChange, onUniversalExpireTimerChange, onZoomFactorChange, - platform, removeCustomColor, removeCustomColorOnConversations, resetAllChatColors, @@ -1028,8 +1026,7 @@ export const Preferences = ({ return ( diff --git a/ts/components/TitleBarContainer.tsx b/ts/components/TitleBarContainer.tsx index 27e867da0..273776aef 100644 --- a/ts/components/TitleBarContainer.tsx +++ b/ts/components/TitleBarContainer.tsx @@ -12,6 +12,7 @@ import { createTemplate } from '../../app/menu'; import { ThemeType } from '../types/Util'; import type { LocaleMessagesType } from '../types/I18N'; import type { MenuOptionsType, MenuActionType } from '../types/menu'; +import { useIsWindowActive } from '../hooks/useIsWindowActive'; export type MenuPropsType = Readonly<{ hasMenu: true; @@ -28,9 +29,8 @@ export type PropsType = Readonly<{ theme: ThemeType; isMaximized?: boolean; isFullScreen?: boolean; - isWindows11: boolean; + hasCustomTitleBar: boolean; hideMenuBar?: boolean; - platform: string; executeMenuRole: ExecuteMenuRoleType; titleBarDoubleClick?: () => void; children: ReactNode; @@ -116,16 +116,17 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => { theme, isMaximized, isFullScreen, - isWindows11, + hasCustomTitleBar, hideMenuBar, executeMenuRole, titleBarDoubleClick, children, hasMenu, - platform, iconSrc = 'images/icon_32.png', } = props; + const isWindowActive = useIsWindowActive(); + const titleBarTheme = useMemo( () => ({ bar: { @@ -201,7 +202,7 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => { [theme, hideMenuBar] ); - if (platform !== 'win32' || isFullScreen) { + if (!hasCustomTitleBar || isFullScreen) { return <>{children}; } @@ -236,17 +237,18 @@ export const TitleBarContainer = (props: PropsType): JSX.Element => { } return ( -
- +
+
{children}
- // Add a pixel of padding on non-maximized Windows 11 titlebar. - isWindows11 && !isMaximized - ? 'TitleBarContainer__title--extra-padding' - : null - )} - platform={platform} + { onDoubleClick={titleBarDoubleClick} hideControls /> - -
{children}
); }; diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 22268d5ad..4ba4cf161 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -36,11 +36,6 @@ export default { // eslint-disable-next-line const noop = () => {}; -Object.assign(window, { - registerForActive: noop, - unregisterForActive: noop, -}); - const items: Record = { 'id-1': { type: 'message', diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 451dc3989..145f4aebe 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -573,7 +573,9 @@ export class Timeline extends React.Component< this.updateIntersectionObserver(); - window.registerForActive(this.markNewestBottomVisibleMessageRead); + window.SignalContext.activeWindowService.registerForActive( + this.markNewestBottomVisibleMessageRead + ); this.delayedPeekTimeout = setTimeout(() => { const { id, peekGroupCallForTheFirstTime } = this.props; @@ -590,7 +592,9 @@ export class Timeline extends React.Component< public override componentWillUnmount(): void { const { delayedPeekTimeout, peekInterval } = this; - window.unregisterForActive(this.markNewestBottomVisibleMessageRead); + window.SignalContext.activeWindowService.unregisterForActive( + this.markNewestBottomVisibleMessageRead + ); this.intersectionObserver?.disconnect(); diff --git a/ts/hooks/useIsWindowActive.ts b/ts/hooks/useIsWindowActive.ts new file mode 100644 index 000000000..046c2dac1 --- /dev/null +++ b/ts/hooks/useIsWindowActive.ts @@ -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; +} diff --git a/ts/manage_full_screen_class.ts b/ts/manage_full_screen_class.ts index 4184cbe63..59751de51 100644 --- a/ts/manage_full_screen_class.ts +++ b/ts/manage_full_screen_class.ts @@ -1,10 +1,14 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // 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('maximized', isMaximized); }; - updateFullScreenClass(window.isFullScreen()); + updateFullScreenClass(window.isFullScreen(), window.isMaximized()); window.onFullScreenChange = updateFullScreenClass; } diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index d9a37ffae..b1dfe82fd 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1417,7 +1417,7 @@ export class ConversationModel extends window.Backbone messagesAdded({ conversationId, messages: [{ ...message.attributes }], - isActive: window.isActive(), + isActive: window.SignalContext.activeWindowService.isActive(), isJustSent, isNewMessage: true, }); @@ -1567,7 +1567,7 @@ export class ConversationModel extends window.Backbone messages: cleaned.map((messageModel: MessageModel) => ({ ...messageModel.attributes, })), - isActive: window.isActive(), + isActive: window.SignalContext.activeWindowService.isActive(), isJustSent: false, isNewMessage: false, }); @@ -1620,7 +1620,7 @@ export class ConversationModel extends window.Backbone messages: cleaned.map((messageModel: MessageModel) => ({ ...messageModel.attributes, })), - isActive: window.isActive(), + isActive: window.SignalContext.activeWindowService.isActive(), isJustSent: false, isNewMessage: false, }); diff --git a/ts/services/ActiveWindowService.ts b/ts/services/ActiveWindowService.ts index 93f989ec9..a63420e33 100644 --- a/ts/services/ActiveWindowService.ts +++ b/ts/services/ActiveWindowService.ts @@ -25,6 +25,8 @@ export class ActiveWindowService { private activeCallbacks: Array<() => void> = []; + private changeCallbacks: Array<(isActive: boolean) => void> = []; + private lastActiveEventAt = -Infinity; 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 { this.updateState(() => { this.lastActiveEventAt = Date.now(); @@ -93,5 +105,11 @@ export class ActiveWindowService { if (!wasActiveBefore && isActiveNow) { this.callActiveCallbacks(); } + + if (wasActiveBefore !== isActiveNow) { + for (const callback of this.changeCallbacks) { + callback(isActiveNow); + } + } } } diff --git a/ts/services/notifications.ts b/ts/services/notifications.ts index 04c766193..b7b62fe99 100644 --- a/ts/services/notifications.ts +++ b/ts/services/notifications.ts @@ -228,7 +228,7 @@ class NotificationService extends EventEmitter { } const { notificationData } = this; - const isAppFocused = window.isActive(); + const isAppFocused = window.SignalContext.activeWindowService.isActive(); const userSetting = this.getNotificationSetting(); // This isn't a boolean because TypeScript isn't smart enough to know that, if diff --git a/ts/set_os_class.ts b/ts/set_os_class.ts index 297f343e8..cf9f0c3b5 100644 --- a/ts/set_os_class.ts +++ b/ts/set_os_class.ts @@ -14,4 +14,8 @@ } document.body.classList.add(className); + + if (window.SignalContext.OS.hasCustomTitleBar()) { + document.body.classList.add('os-has-custom-titlebar'); + } } diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index 49e36078d..47b50e3a6 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -22,7 +22,6 @@ import { getIsMainWindowMaximized, getIsMainWindowFullScreen, getMenuOptions, - getPlatform, } from '../selectors/user'; import { shouldShowStoriesView } from '../selectors/stories'; import { getHideMenuBar } from '../selectors/items'; @@ -42,8 +41,7 @@ const mapStateToProps = (state: StateType) => { isMaximized: getIsMainWindowMaximized(state), isFullScreen: getIsMainWindowFullScreen(state), menuOptions: getMenuOptions(state), - platform: getPlatform(state), - isWindows11: window.SignalContext.OS.isWindows11(), + hasCustomTitleBar: window.SignalContext.OS.hasCustomTitleBar(), hideMenuBar: getHideMenuBar(state), renderCallManager: () => , renderCustomizingPreferredReactionsModal: () => ( diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx index c5eed54ea..2c812e76d 100644 --- a/ts/state/smart/CallManager.tsx +++ b/ts/state/smart/CallManager.tsx @@ -53,7 +53,8 @@ async function notifyForCall( isVideoCall: boolean ): Promise { const shouldNotify = - !window.isActive() && window.Events.getCallSystemNotification(); + !window.SignalContext.activeWindowService.isActive() && + window.Events.getCallSystemNotification(); if (!shouldNotify) { return; } diff --git a/ts/types/RendererConfig.ts b/ts/types/RendererConfig.ts index 095f2bfed..54e19edb5 100644 --- a/ts/types/RendererConfig.ts +++ b/ts/types/RendererConfig.ts @@ -92,6 +92,7 @@ export const rendererConfigSchema = z.object({ // Only used by main window isMainWindowFullScreen: z.boolean(), + isMainWindowMaximized: z.boolean(), // Only for tests argv: configOptionalStringSchema, diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index 0df46127a..775268b3c 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -450,7 +450,7 @@ export class ConversationView extends window.Backbone.View { }; const markMessageRead = async (messageId: string) => { - if (!window.isActive()) { + if (!window.SignalContext.activeWindowService.isActive()) { return; } diff --git a/ts/window.d.ts b/ts/window.d.ts index fba8fe74e..fc4b6362a 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -291,10 +291,10 @@ declare global { waitForEmptyEventQueue: () => Promise; getVersion: () => string; i18n: LocalizerType; - isActive: () => boolean; isAfterVersion: (version: string, anotherVersion: string) => boolean; isBeforeVersion: (version: string, anotherVersion: string) => boolean; isFullScreen: () => boolean; + isMaximized: () => boolean; initialTheme?: ThemeType; libphonenumberInstance: { parse: (number: string) => PhoneNumber; @@ -303,12 +303,11 @@ declare global { }; libphonenumberFormat: typeof PhoneNumberFormat; nodeSetImmediate: typeof setImmediate; - onFullScreenChange: (fullScreen: boolean) => void; + onFullScreenChange: (fullScreen: boolean, maximized: boolean) => void; platform: string; preloadedImages: Array; reduxActions: ReduxActions; reduxStore: Store; - registerForActive: (handler: () => void) => void; restart: () => void; setImmediate: typeof setImmediate; showWindow: () => void; @@ -326,7 +325,6 @@ declare global { systemTheme: WhatIsThis; textsecure: typeof textsecure; titleBarDoubleClick: () => void; - unregisterForActive: (handler: () => void) => void; updateTrayIcon: (count: number) => void; Backbone: typeof Backbone; CI?: CI; diff --git a/ts/windows/about/preload.ts b/ts/windows/about/preload.ts index 8ca4793fc..41b2fdbb3 100644 --- a/ts/windows/about/preload.ts +++ b/ts/windows/about/preload.ts @@ -36,8 +36,7 @@ contextBridge.exposeInMainWorld('SignalContext', { environment: `${environmentText.join(' - ')}${platform}`, i18n: SignalContext.i18n, version: SignalContext.getVersion(), - platform: process.platform, - isWindows11: SignalContext.OS.isWindows11(), + hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(), executeMenuRole: SignalContext.executeMenuRole, }), document.getElementById('app') diff --git a/ts/windows/context.ts b/ts/windows/context.ts index b0f6f7741..20cc6f730 100644 --- a/ts/windows/context.ts +++ b/ts/windows/context.ts @@ -12,6 +12,7 @@ import type { LocaleMessagesType } from '../types/I18N'; import type { NativeThemeType } from '../context/createNativeThemeListener'; import type { SettingType } from '../util/preload'; import type { RendererConfigType } from '../types/RendererConfig'; +import { ActiveWindowService } from '../services/ActiveWindowService'; import { Bytes } from '../context/Bytes'; 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 { waitForSettingsChange } from './waitForSettingsChange'; 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 configParam = params.get('config'); @@ -58,6 +62,7 @@ export type SignalContextType = { nativeThemeListener: NativeThemeType; setIsCallActive: (isCallActive: boolean) => unknown; + activeWindowService: typeof activeWindowService; Settings: { themeSetting: SettingType; waitForChange: () => Promise; @@ -65,9 +70,9 @@ export type SignalContextType = { OS: { platform: string; isWindows: typeof isWindows; - isWindows11: typeof isWindows11; isLinux: typeof isLinux; isMacOS: typeof isMacOS; + hasCustomTitleBar: typeof hasCustomTitleBar; }; config: RendererConfigType; getAppInstance: () => string | undefined; @@ -86,6 +91,7 @@ export type SignalContextType = { }; export const SignalContext: SignalContextType = { + activeWindowService, Settings: { themeSetting: createSetting('themeSetting', { setter: false }), waitForChange: waitForSettingsChange, @@ -93,9 +99,9 @@ export const SignalContext: SignalContextType = { OS: { platform: process.platform, isWindows, - isWindows11, isLinux, isMacOS, + hasCustomTitleBar, }, bytes: new Bytes(), config, diff --git a/ts/windows/debuglog/preload.ts b/ts/windows/debuglog/preload.ts index ace528af0..a395e9834 100644 --- a/ts/windows/debuglog/preload.ts +++ b/ts/windows/debuglog/preload.ts @@ -26,8 +26,7 @@ contextBridge.exposeInMainWorld('SignalContext', { ReactDOM.render( React.createElement(DebugLogWindow, { - platform: process.platform, - isWindows11: SignalContext.OS.isWindows11(), + hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(), executeMenuRole: SignalContext.executeMenuRole, closeWindow: () => SignalContext.executeMenuRole('close'), downloadLog: (logText: string) => diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 636aee433..e28e8a583 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -254,14 +254,17 @@ window.sendChallengeRequest = request => ipc.send('challenge:request', request); { let isFullScreen = Boolean(config.isMainWindowFullScreen); + let isMaximized = Boolean(config.isMainWindowMaximized); window.isFullScreen = () => isFullScreen; + window.isMaximized = () => isMaximized; // This is later overwritten. window.onFullScreenChange = noop; - ipc.on('full-screen-change', (_event, isFull) => { - isFullScreen = Boolean(isFull); - window.onFullScreenChange(isFullScreen); + ipc.on('window:set-window-stats', (_event, stats) => { + isFullScreen = Boolean(stats.isFullScreen); + isMaximized = Boolean(stats.isMaximized); + window.onFullScreenChange(isFullScreen, isMaximized); }); } diff --git a/ts/windows/main/phase2-dependencies.ts b/ts/windows/main/phase2-dependencies.ts index baae91ffa..18936303c 100644 --- a/ts/windows/main/phase2-dependencies.ts +++ b/ts/windows/main/phase2-dependencies.ts @@ -1,7 +1,6 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { ipcRenderer as ipc } from 'electron'; import Backbone from 'backbone'; import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'; import * as React from 'react'; @@ -12,7 +11,6 @@ import PQueue from 'p-queue'; import { textsecure } from '../../textsecure'; import { imageToBlurHash } from '../../util/imageToBlurHash'; -import { ActiveWindowService } from '../../services/ActiveWindowService'; import * as Attachments from '../attachments'; import { setup } from '../../signal'; import { addSensitivePath } from '../../util/privacy'; @@ -44,14 +42,6 @@ window.imageToBlurHash = imageToBlurHash; window.libphonenumberInstance = PhoneNumberUtil.getInstance(); 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.ReactDOM = ReactDOM; window.PQueue = PQueue; diff --git a/ts/windows/settings/preload.ts b/ts/windows/settings/preload.ts index 481eab411..323d79937 100644 --- a/ts/windows/settings/preload.ts +++ b/ts/windows/settings/preload.ts @@ -341,8 +341,7 @@ const renderPreferences = async () => { i18n: SignalContext.i18n, - platform: process.platform, - isWindows11: SignalContext.OS.isWindows11(), + hasCustomTitleBar: SignalContext.OS.hasCustomTitleBar(), executeMenuRole: SignalContext.executeMenuRole, };