Electron: Various improvements (#3310)
This commit is contained in:
parent
7daee68c0e
commit
7dd9c2e677
10
README.md
10
README.md
@ -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
1127
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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 |
@ -31,6 +31,12 @@
|
||||
height: 10rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
body.is-electron & {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
#logo {
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
42
src/electron/autoUpdates.ts
Normal file
42
src/electron/autoUpdates.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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"]
|
||||
|
||||
@ -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')));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@ -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
205
src/electron/windowState.ts
Normal 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;
|
||||
@ -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 {
|
||||
|
||||
@ -694,7 +694,7 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
|
||||
}
|
||||
}
|
||||
|
||||
setPageTitleInstant(PAGE_TITLE);
|
||||
setPageTitleInstant(IS_ELECTRON ? '' : PAGE_TITLE);
|
||||
});
|
||||
|
||||
let prevIsScreenLocked: boolean | undefined;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user