Electron: Various improvements (#3310)

This commit is contained in:
Alexander Zinchuk 2023-07-20 15:58:07 +02:00
parent 7daee68c0e
commit 7dd9c2e677
28 changed files with 1157 additions and 471 deletions

View File

@ -43,7 +43,7 @@ Electron allows building a native application that can be installed on Windows,
#### NPM scripts
- `npm run electron:dev`
- `npm run dev:electron`
Run Electron in development mode, concurrently starts 3 processes with watch for changes: main (main Electron process), renderer (FE code) and Webpack for Electron (compiles main Electron process from TypeScript).
@ -55,15 +55,15 @@ The main process code for Electron, which includes preload functionality, is wri
Prepare renderer (FE code) build, compile Electron main process code, install and build native dependencies, is used before packaging or publishing.
- `npm run electron:package:staging`
- `npm run electron:staging`
Create packages for macOS, Windows and Linux in `dist-electron` folders with `APP_ENV` as `staging` (allows to open DevTools, includes sourcemaps and does not minify built JavaScript code), can be used for manual distribution and testing packaged application.
- `npm run electron:package:production`
- `npm run electron:production`
Create packages for macOS, Windows and Linux in `dist-electron` folders with `APP_ENV` as `production` (disabled DevTools, minified built JavaScript code), can be used for manual distribution and testing packaged application.
- `npm run electron:publish`
- `npm run deploy:electron`
Create packages for macOS, Windows and Linux in `dist-electron` folder and publish release to GitHub, which allows supporting autoupdates. See [GitHub release workflow](#github-release) for more info.
@ -80,7 +80,7 @@ More info in the [official documentation](https://www.electronjs.org/docs/latest
#### Notarize on MacOS
Application notarization is done with [electron-builder-notarize](https://github.com/karaggeorge/electron-builder-notarize) module, which requires `APPLE_ID` and `APPLE_ID_PASSWORD` environment variables to be passed.
Application notarization is done automatically in [electron-builder](https://github.com/electron-userland/electron-builder/) module, which requires `APPLE_ID` and `APPLE_APP_SPECIFIC_PASSWORD` environment variables to be passed.
How to obtain app-specific password:

1127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -60,7 +60,6 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/register": "^7.21.0",
"@electron/rebuild": "^3.2.10",
"@glen/jest-raw-loader": "^2.0.0",
"@playwright/test": "^1.31.2",
"@statoscope/cli": "^5.26.1",
@ -86,10 +85,10 @@
"css-loader": "^6.7.3",
"dotenv": "^16.0.3",
"electron": "^22.0.0",
"electron-builder": "^23.6.0",
"electron-builder-notarize": "^1.5.1",
"electron-builder": "^24.5.2",
"electron-context-menu": "^3.6.1",
"electron-store": "^8.1.0",
"electron-updater": "^5.3.0",
"electron-window-state": "^5.0.3",
"electronmon": "^2.0.2",
"eslint": "^8.36.0",
"eslint-config-airbnb": "^19.0.4",

View File

@ -2,11 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.print</key>
<true />
<key>com.apple.security.personal-information.location</key>
<true/>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -31,6 +31,12 @@
height: 10rem;
margin-bottom: 2.5rem;
}
body.is-electron & {
width: 6rem;
height: 6rem;
margin-bottom: 1.75rem;
}
}
#logo {

View File

@ -28,9 +28,11 @@ const ActiveCallHeader: FC<StateProps> = ({
useEffect(() => {
document.body.classList.toggle('has-call-header', Boolean(isCallPanelVisible));
window.electron?.setTrafficLightPosition(isCallPanelVisible ? 'lowered' : 'standard');
return () => {
document.body.classList.toggle('has-call-header', false);
window.electron?.setTrafficLightPosition('standard');
};
}, [isCallPanelVisible]);

View File

@ -81,6 +81,10 @@
&.scrolled {
border-bottom-color: var(--group-call-panel-header-border-color);
}
:global(body.is-electron) .root.fullscreen:not(.landscape) & {
padding-left: 5rem;
}
}
.headerButton {

View File

@ -8,6 +8,8 @@ import React, {
useEffect, useRef, memo, useState,
} from '../../lib/teact/teact';
import { IS_ELECTRON } from '../../config';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import generateUniqueId from '../../util/generateUniqueId';
@ -255,7 +257,7 @@ const AnimatedSticker: FC<OwnProps> = ({
className={buildClassName('AnimatedSticker', className)}
style={buildStyle(
size !== undefined && `width: ${size}px; height: ${size}px;`,
onClick && 'cursor: pointer',
onClick && !IS_ELECTRON && 'cursor: pointer',
colorFilter,
style,
)}

View File

@ -3,6 +3,8 @@ import { getActions } from '../../../global';
import type { ActiveEmojiInteraction } from '../../../global/types';
import { IS_ELECTRON } from '../../../config';
import safePlay from '../../../util/safePlay';
import buildStyle from '../../../util/buildStyle';
import { REM } from '../helpers/mediaDimensions';
@ -37,7 +39,7 @@ export default function useAnimatedEmoji(
const soundMediaData = useMedia(soundId ? `document${soundId}` : undefined, !soundId);
const size = preferredSize || SIZE;
const style = buildStyle(`width: ${size}px`, `height: ${size}px`, emoji && 'cursor: pointer');
const style = buildStyle(`width: ${size}px`, `height: ${size}px`, emoji && !IS_ELECTRON && 'cursor: pointer');
const interactions = useRef<number[] | undefined>(undefined);
const startedInteractions = useRef<number | undefined>(undefined);

View File

@ -21,7 +21,7 @@ import {
IS_TEST,
PRODUCTION_HOSTNAME,
} from '../../../config';
import { IS_APP } from '../../../util/windowEnvironment';
import { IS_APP, IS_MAC_OS } from '../../../util/windowEnvironment';
import {
INITIAL_PERFORMANCE_STATE_MAX,
INITIAL_PERFORMANCE_STATE_MID,
@ -421,7 +421,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
)}
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
transformOriginX={IS_ELECTRON && !isFullscreen ? 90 : undefined}
transformOriginX={IS_ELECTRON && IS_MAC_OS && !isFullscreen ? 90 : undefined}
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
>
{menuItems}

View File

@ -282,11 +282,11 @@ const Main: FC<OwnProps & StateProps> = ({
return undefined;
}
const removeUpdateDownloadedListener = window.electron?.on(ElectronEvent.UPDATE_DOWNLOADED, () => {
const removeUpdateDownloadedListener = window.electron!.on(ElectronEvent.UPDATE_DOWNLOADED, () => {
setIsAppUpdateAvailable(true);
});
const removeUpdateErrorListener = window.electron?.on(ElectronEvent.UPDATE_ERROR, () => {
const removeUpdateErrorListener = window.electron!.on(ElectronEvent.UPDATE_ERROR, () => {
setIsAppUpdateAvailable(false);
removeUpdateDownloadedListener?.();
});
@ -501,7 +501,7 @@ const Main: FC<OwnProps & StateProps> = ({
});
// Online status and browser tab indicators
useBackgroundMode(handleBlur, handleFocus);
useBackgroundMode(handleBlur, handleFocus, !!IS_ELECTRON);
useBeforeUnload(handleBlur);
usePreventPinchZoomGesture(isMediaViewerOpen);

View File

@ -5,7 +5,11 @@
top: 1rem;
height: 1rem;
touch-action: none;
cursor: pointer;
cursor: var(--custom-cursor, pointer);
:global(body.is-electron) & {
cursor: auto;
}
}
.preview {

View File

@ -47,6 +47,14 @@
margin-left: auto;
}
body.is-electron.is-macos & {
-webkit-app-region: drag;
.SearchInput {
-webkit-app-region: no-drag;
}
}
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
@media (max-width: 600px) {
padding-left: 5rem;

View File

@ -0,0 +1,42 @@
import { ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import type { UpdateInfo } from 'electron-updater';
import { autoUpdater } from 'electron-updater';
import { ElectronAction, ElectronEvent } from '../types/electron';
import { IS_MAC_OS, forceQuit, windows } from './utils';
import type { WindowState } from './windowState';
let interval: NodeJS.Timer;
const CHECK_UPDATE_INTERVAL = 5 * 60 * 1000;
export default function setupAutoUpdates(window: BrowserWindow, state: WindowState) {
if (!interval) {
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.checkForUpdates();
interval = setInterval(() => autoUpdater.checkForUpdates(), CHECK_UPDATE_INTERVAL);
ipcMain.handle(ElectronAction.INSTALL_UPDATE, () => {
state.saveLastUrlHash();
if (IS_MAC_OS) {
forceQuit.enable();
}
return autoUpdater.quitAndInstall();
});
}
autoUpdater.on('error', (error: Error) => {
if (windows.has(window)) {
window.webContents.send(ElectronEvent.UPDATE_ERROR, error);
}
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
if (windows.has(window)) {
window.webContents.send(ElectronEvent.UPDATE_DOWNLOADED, info);
}
});
}

View File

@ -1,9 +1,9 @@
productName: "Telegram A"
artifactName: "${productName}-${arch}-${version}.${ext}"
artifactName: "${productName}-${arch}.${ext}"
appId: "org.telegram.TelegramA"
afterSign: "electron-builder-notarize"
extraMetadata:
main: "./dist/electron.js"
productName: "Telegram A"
files:
- "dist"
- "package.json"
@ -20,15 +20,22 @@ publish:
provider: "github"
owner: "Ajaxy"
repo: "telegram-tt"
releaseType: "release"
win:
target: "nsis"
icon: "public/icon-electron-windows.ico"
nsis:
oneClick: false
createDesktopShortcut: true
createStartMenuShortcut: true
mac:
target:
target: "default"
arch: ["x64", "arm64"]
entitlements: "public/electron-entitlements.mac.plist"
icon: "public/icon-electron-macos.icns"
notarize:
teamId: "Y54Z4K69Z9"
linux:
category: "Community"
target: ["AppImage"]

View File

@ -1,11 +1,20 @@
import 'v8-compile-cache';
import { app, nativeImage } from 'electron';
import contextMenu from 'electron-context-menu';
import path from 'path';
import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window';
import { IS_MAC_OS, IS_WINDOWS } from './utils';
contextMenu({
showLearnSpelling: false,
showLookUpSelection: false,
showSearchWithGoogle: false,
showCopyImage: false,
showSelectAll: true,
});
app.on('ready', () => {
if (IS_MAC_OS) {
app.dock.setIcon(nativeImage.createFromPath(path.resolve(__dirname, '../public/icon-electron-macos.png')));

View File

@ -1,7 +1,7 @@
import { contextBridge, ipcRenderer } from 'electron';
import type { IpcRendererEvent } from 'electron';
import type { ElectronApi, ElectronEvent } from '../types/electron';
import type { ElectronApi, ElectronEvent, TrafficLightPosition } from '../types/electron';
import { ElectronAction } from '../types/electron';
const electronApi: ElectronApi = {
@ -9,6 +9,9 @@ const electronApi: ElectronApi = {
installUpdate: () => ipcRenderer.invoke(ElectronAction.INSTALL_UPDATE),
handleDoubleClick: () => ipcRenderer.invoke(ElectronAction.HANDLE_DOUBLE_CLICK),
openNewWindow: (url: string) => ipcRenderer.invoke(ElectronAction.OPEN_NEW_WINDOW, url),
setWindowTitle: (title?: string) => ipcRenderer.invoke(ElectronAction.SET_WINDOW_TITLE, title),
setTrafficLightPosition:
(position: TrafficLightPosition) => ipcRenderer.invoke(ElectronAction.SET_TRAFFIC_LIGHT_POSITION, position),
on: (eventName: ElectronEvent, callback) => {
const subscription = (event: IpcRendererEvent, ...args: any) => callback(...args);

View File

@ -1,3 +1,41 @@
import { app } from 'electron';
import type { BrowserWindow, Point } from 'electron';
import type { TrafficLightPosition } from '../types/electron';
export const windows = new Set<BrowserWindow>();
export const IS_MAC_OS = process.platform === 'darwin';
export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux';
export function getAppTitle(chatTitle?: string): string {
const appName = app.getName();
if (!chatTitle) {
return appName;
}
return `${chatTitle} · ${appName}`;
}
export const TRAFFIC_LIGHT_POSITION: Record<TrafficLightPosition, Point> = {
standard: { x: 10, y: 20 },
lowered: { x: 10, y: 52 },
};
export const forceQuit = {
value: false,
enable() {
this.value = true;
},
disable() {
this.value = false;
},
get isEnabled(): boolean {
return this.value;
},
};

View File

@ -2,24 +2,22 @@ import {
app, BrowserWindow, ipcMain, shell, systemPreferences,
} from 'electron';
import type { HandlerDetails } from 'electron';
import type { UpdateInfo } from 'electron-updater';
import { autoUpdater } from 'electron-updater';
import windowStateKeeper from 'electron-window-state';
import path from 'path';
import { ElectronAction, ElectronEvent } from '../types/electron';
import { IS_MAC_OS } from './utils';
import type { TrafficLightPosition } from '../types/electron';
import setupAutoUpdates from './autoUpdates';
import {
forceQuit, getAppTitle, IS_MAC_OS, TRAFFIC_LIGHT_POSITION, windows,
} from './utils';
import windowStateKeeper from './windowState';
let forceQuit = false;
let interval: NodeJS.Timer;
const windows = new Set<BrowserWindow>();
const CHECK_UPDATE_INTERVAL = 10 * 60 * 1000;
const ALLOWED_DEVICE_ORIGINS = ['http://localhost:1234', 'file://'];
export function createWindow(url?: string) {
const windowState = windowStateKeeper({
defaultWidth: 1088,
defaultHeight: IS_MAC_OS ? 700 : 750,
defaultHeight: 700,
});
let x;
@ -55,14 +53,14 @@ export function createWindow(url?: string) {
minWidth: 360,
width,
height,
title: 'Telegram A',
title: getAppTitle(),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
devTools: process.env.APP_ENV !== 'production',
},
...(IS_MAC_OS && {
titleBarStyle: 'hidden',
trafficLightPosition: { x: 10, y: 20 },
trafficLightPosition: TRAFFIC_LIGHT_POSITION.standard,
}),
});
@ -77,6 +75,10 @@ export function createWindow(url?: string) {
return { action: 'deny' };
});
window.webContents.session.setDevicePermissionHandler(({ deviceType, origin }) => {
return deviceType === 'hid' && ALLOWED_DEVICE_ORIGINS.includes(origin);
});
window.on('enter-full-screen', () => {
window.webContents.send(ElectronEvent.FULLSCREEN_CHANGE, true);
});
@ -87,14 +89,15 @@ export function createWindow(url?: string) {
window.on('close', (event) => {
if (IS_MAC_OS) {
if (forceQuit) {
if (forceQuit.isEnabled) {
app.exit(0);
forceQuit = false;
forceQuit.disable();
} else {
const hasExtraWindows = BrowserWindow.getAllWindows().length > 1;
if (hasExtraWindows) {
windows.delete(window);
windowState.unmanage();
} else {
event.preventDefault();
window.hide();
@ -106,12 +109,14 @@ export function createWindow(url?: string) {
if (url) {
window.loadURL(url);
} else if (app.isPackaged) {
window.loadURL(`file://${__dirname}/index.html`);
window.loadURL(`file://${__dirname}/index.html${windowState.urlHash}`);
} else {
window.loadURL('http://localhost:1234');
window.loadURL(`http://localhost:1234${windowState.urlHash}`);
window.webContents.openDevTools();
}
windowState.clearLastUrlHash();
if (!IS_MAC_OS) {
window.removeMenu();
}
@ -120,45 +125,21 @@ export function createWindow(url?: string) {
window.show();
if (process.env.APP_ENV === 'production') {
setupAutoUpdates(window);
setupAutoUpdates(window, windowState);
}
});
windows.add(window);
}
function setupAutoUpdates(window: BrowserWindow) {
if (!interval) {
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.checkForUpdates();
interval = setInterval(() => autoUpdater.checkForUpdates(), CHECK_UPDATE_INTERVAL);
ipcMain.handle(ElectronAction.INSTALL_UPDATE, () => {
if (IS_MAC_OS) {
forceQuit = true;
}
return autoUpdater.quitAndInstall();
});
}
autoUpdater.on('error', (error: Error) => {
if (windows.has(window)) {
window.webContents.send(ElectronEvent.UPDATE_ERROR, error);
}
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
if (windows.has(window)) {
window.webContents.send(ElectronEvent.UPDATE_DOWNLOADED, info);
}
});
}
export function setupElectronActionHandlers() {
ipcMain.handle(ElectronAction.OPEN_NEW_WINDOW, (_, newWindowUrl: string) => {
createWindow(newWindowUrl);
ipcMain.handle(ElectronAction.OPEN_NEW_WINDOW, (_, url: string) => {
createWindow(url);
});
ipcMain.handle(ElectronAction.SET_WINDOW_TITLE, (_, newTitle?: string) => {
const currentWindow = BrowserWindow.getFocusedWindow();
currentWindow?.setTitle(getAppTitle(newTitle));
});
ipcMain.handle(ElectronAction.GET_IS_FULLSCREEN, () => {
@ -180,6 +161,15 @@ export function setupElectronActionHandlers() {
}
}
});
ipcMain.handle(ElectronAction.SET_TRAFFIC_LIGHT_POSITION, (_, position: TrafficLightPosition) => {
if (!IS_MAC_OS) {
return;
}
const currentWindow = BrowserWindow.getFocusedWindow();
currentWindow?.setTrafficLightPosition(TRAFFIC_LIGHT_POSITION[position]);
});
}
export function setupCloseHandlers() {
@ -190,9 +180,9 @@ export function setupCloseHandlers() {
});
app.on('before-quit', (event) => {
if (IS_MAC_OS && !forceQuit) {
if (IS_MAC_OS && !forceQuit.isEnabled) {
event.preventDefault();
forceQuit = true;
forceQuit.enable();
app.quit();
}
});
@ -203,7 +193,7 @@ export function setupCloseHandlers() {
if (!hasActiveWindow) {
createWindow();
} else if (IS_MAC_OS) {
forceQuit = false;
forceQuit.disable();
const currentWindow = Array.from(windows).pop();
currentWindow?.show();

205
src/electron/windowState.ts Normal file
View File

@ -0,0 +1,205 @@
import type { BrowserWindow, Rectangle } from 'electron';
import { screen } from 'electron';
import Store from 'electron-store';
type Options = {
defaultHeight?: number;
defaultWidth?: number;
fullScreen?: boolean;
maximize?: boolean;
};
type State = {
displayBounds: {
height: number;
width: number;
};
width: number;
height: number;
x: number;
y: number;
isFullScreen: boolean;
isMaximized: boolean;
urlHash: string;
};
export type WindowState = State & {
manage: (window: Electron.BrowserWindow) => void;
unmanage: () => void;
resetStateToDefault: () => void;
saveLastUrlHash: () => void;
clearLastUrlHash: () => void;
};
const EVENT_HANDLING_DELAY = 100;
const STORE_KEY = 'window-state';
const DEFAULT_OPTIONS = {
defaultHeight: 600,
defaultWidth: 800,
maximize: true,
fullScreen: true,
};
const store: Store = new Store();
function windowStateKeeper(options: Options): WindowState {
let state: State;
let winRef: BrowserWindow | undefined;
let stateChangeTimer: NodeJS.Timer | number;
options = {
...DEFAULT_OPTIONS,
...options,
};
function isNormal(win: BrowserWindow): boolean {
return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen();
}
function hasBounds(): boolean {
return state
&& Number.isInteger(state.x)
&& Number.isInteger(state.y)
&& Number.isInteger(state.width) && state.width > 0
&& Number.isInteger(state.height) && state.height > 0;
}
function resetStateToDefault() {
const displayBounds = screen.getPrimaryDisplay().bounds;
state = {
width: options.defaultWidth!,
height: options.defaultHeight!,
x: 0,
y: 0,
displayBounds,
isMaximized: false,
isFullScreen: false,
urlHash: '',
};
}
function windowWithinBounds(bounds: Rectangle) {
return state.x >= bounds.x
&& state.y >= bounds.y
&& state.x + state.width <= bounds.x + bounds.width
&& state.y + state.height <= bounds.y + bounds.height;
}
function ensureWindowVisibleOnSomeDisplay() {
const visible = screen.getAllDisplays().some((display) => windowWithinBounds(display.bounds));
if (!visible) {
resetStateToDefault();
}
}
function validateState() {
const isValid = state && (hasBounds() || state.isMaximized || state.isFullScreen);
if (!isValid) {
resetStateToDefault();
return;
}
if (hasBounds() && state.displayBounds) {
ensureWindowVisibleOnSomeDisplay();
}
}
function updateState() {
if (!winRef) {
return;
}
// Don't throw an error when window was closed
try {
const winBounds = winRef.getBounds();
if (isNormal(winRef)) {
state.x = winBounds.x;
state.y = winBounds.y;
state.width = winBounds.width;
state.height = winBounds.height;
}
state.isMaximized = winRef.isMaximized();
state.isFullScreen = winRef.isFullScreen();
state.displayBounds = screen.getDisplayMatching(winBounds).bounds;
} catch (err) {
// Handler not supported, ignoring
}
}
function handleStateChange() {
clearTimeout(stateChangeTimer);
stateChangeTimer = setTimeout(updateState, EVENT_HANDLING_DELAY);
}
function handleClose() {
updateState();
}
function handleClosed() {
unmanage();
store.set(STORE_KEY, state);
}
function manage(win: BrowserWindow) {
if (options.maximize && state.isMaximized) {
win.maximize();
}
if (options.fullScreen && state.isFullScreen) {
win.setFullScreen(true);
}
win.on('resize', handleStateChange);
win.on('move', handleStateChange);
win.on('close', handleClose);
win.on('closed', handleClosed);
winRef = win;
}
function unmanage() {
if (winRef) {
winRef.removeListener('resize', handleStateChange);
winRef.removeListener('move', handleStateChange);
clearTimeout(stateChangeTimer);
winRef.removeListener('close', handleClose);
winRef.removeListener('closed', handleClosed);
winRef = undefined;
}
}
function saveLastUrlHash() {
if (winRef) {
const { hash } = new URL(winRef.webContents.getURL());
state.urlHash = hash;
}
}
function clearLastUrlHash() {
state.urlHash = '';
}
state = store.get(STORE_KEY) as State;
validateState();
return {
get x() { return state.x; },
get y() { return state.y; },
get width() { return state.width; },
get height() { return state.height; },
get displayBounds() { return state.displayBounds; },
get isMaximized() { return state.isMaximized; },
get isFullScreen() { return state.isFullScreen; },
get urlHash() { return state.urlHash || ''; },
unmanage,
manage,
resetStateToDefault,
saveLastUrlHash,
clearLastUrlHash,
};
}
export default windowStateKeeper;

View File

@ -82,6 +82,7 @@ addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnTyp
const { chatId, threadId = MAIN_THREAD_ID } = payload;
const hashUrl = createMessageHashUrl(chatId, 'thread', threadId);
if (IS_ELECTRON) {
window.electron!.openNewWindow(hashUrl);
} else {

View File

@ -694,7 +694,7 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
}
}
setPageTitleInstant(PAGE_TITLE);
setPageTitleInstant(IS_ELECTRON ? '' : PAGE_TITLE);
});
let prevIsScreenLocked: boolean | undefined;

View File

@ -71,6 +71,10 @@ body.is-ios {
--border-radius-messages-small: 0.5rem;
}
body.is-electron {
--custom-cursor: default;
}
body.cursor-grabbing {
--custom-cursor: grabbing;
cursor: grabbing !important;

View File

@ -9,13 +9,19 @@ export enum ElectronAction {
INSTALL_UPDATE = 'install-update',
HANDLE_DOUBLE_CLICK = 'handle-double-click',
OPEN_NEW_WINDOW = 'open-new-window',
SET_WINDOW_TITLE = 'set-window-title',
SET_TRAFFIC_LIGHT_POSITION = 'set-traffic-light-position',
}
export type TrafficLightPosition = 'standard' | 'lowered';
export interface ElectronApi {
isFullscreen: () => Promise<boolean>;
installUpdate: () => Promise<void>;
handleDoubleClick: () => Promise<void>;
openNewWindow: (url: string) => Promise<void>;
openNewWindow: (url: string, title?: string) => Promise<void>;
setWindowTitle: (title?: string) => Promise<void>;
setTrafficLightPosition: (position: TrafficLightPosition) => Promise<void>;
on: (eventName: ElectronEvent, callback: any) => VoidFunction;
}

View File

@ -346,9 +346,9 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
!isScreenLocked
&& selectShouldShowMessagePreview(chat, selectNotifySettings(global), selectNotifyExceptions(global))
) {
if (isActionMessage(message)) {
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId);
if (isActionMessage(message)) {
body = renderActionMessageText(
translate,
message,

View File

@ -1,3 +1,4 @@
import { IS_ELECTRON } from '../config';
import { debounce } from './schedulers';
const UPDATE_DEBOUNCE_MS = 200;
@ -5,6 +6,12 @@ const UPDATE_DEBOUNCE_MS = 200;
// For some reason setting `document.title` to the same value
// causes increment of Chrome Dev Tools > Performance Monitor > DOM Nodes counter
export function setPageTitleInstant(nextTitle: string) {
if (IS_ELECTRON) {
window.electron!.setWindowTitle(nextTitle);
return;
}
if (document.title !== nextTitle) {
document.title = nextTitle;
}