[electron] Include bindings in macOS build (#5937)

This commit is contained in:
zubiden 2025-06-04 20:41:00 +02:00 committed by Alexander Zinchuk
parent b4772cc542
commit ee7f2e1c8c
12 changed files with 220 additions and 1087 deletions

View File

@ -41,7 +41,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true' if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci run: npm ci --include=dev # Hack: install `electron-drag-click` as a dev dependency, so build happens only on macOS
- name: Import MacOS signing certificate - name: Import MacOS signing certificate
env: env:

View File

@ -43,7 +43,7 @@ Electron allows building a native application that can be installed on Windows,
#### NPM scripts #### NPM scripts
- `npm run dev:electron` - `npm run electron:dev`
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). 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).

1055
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,10 @@
"build:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 npm run build:dev", "build:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 npm run build:dev",
"build:production": "webpack && bash ./deploy/copy_to_dist.sh", "build:production": "webpack && bash ./deploy/copy_to_dist.sh",
"web:release:production": "npm i && npm run build:production && git add -A && git commit -a -m '[Build]' --no-verify && git push", "web:release:production": "npm i && npm run build:production && git add -A && git commit -a -m '[Build]' --no-verify && git push",
"electron:dev": "npm run electron:webpack && IS_PACKAGED_ELECTRON=true concurrently -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron.cjs\"", "electron:dev": "npm run electron:webpack && IS_PACKAGED_ELECTRON=true concurrently --ks SIGKILL -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron.cjs\"",
"electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts", "electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts",
"electron:build": "IS_PACKAGED_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && electron-rebuild && ENV=$ENV npm run electron:webpack", "electron:build": "IS_PACKAGED_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && ENV=$ENV npm run electron:webpack",
"electron:package": "npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.yml", "electron:package": "npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.js",
"electron:package:staging": "ENV=staging npm run electron:package -- -p never", "electron:package:staging": "ENV=staging npm run electron:package -- -p never",
"electron:release:production": "ENV=production npm run electron:package -- -p always", "electron:release:production": "ENV=production npm run electron:package -- -p always",
"telegraph:update_changelog": "node ./dev/telegraphChangelog.js", "telegraph:update_changelog": "node ./dev/telegraphChangelog.js",
@ -51,8 +51,7 @@
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "^7.27.1",
"@babel/register": "^7.27.1", "@babel/register": "^7.27.1",
"@electron/rebuild": "^4.0.1", "@eslint/js": "^9.27.0",
"@eslint/js": "^9.26.0",
"@glen/jest-raw-loader": "^2.0.0", "@glen/jest-raw-loader": "^2.0.0",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@statoscope/cli": "5.29.0", "@statoscope/cli": "5.29.0",
@ -66,8 +65,8 @@
"@types/dom-view-transitions": "^1.0.6", "@types/dom-view-transitions": "^1.0.6",
"@types/hast": "^3.0.4", "@types/hast": "^3.0.4",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.15.17", "@types/node": "^22.15.21",
"@types/react": "^19.1.4", "@types/react": "^19.1.5",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"@types/webpack": "^5.28.5", "@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^8.32.1", "@typescript-eslint/eslint-plugin": "^8.32.1",
@ -76,21 +75,22 @@
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"babel-plugin-transform-import-meta": "^2.3.2", "babel-plugin-transform-import-meta": "^2.3.2",
"bindings": "git+https://github.com/zubiden/node-bindings#6b9b0711a12e9df9cbf59a3271c25bf34c306907", "bindings": "git+https://github.com/zubiden/node-bindings#1f689378b1cd26f99d3b7156fe40a520365d1272",
"browserlist": "^1.0.1", "browserlist": "^1.0.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"electron": "^36.2.0", "electron": "^36.2.1",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-conf": "^1.3.0", "electron-conf": "^1.3.0",
"electron-context-menu": "^4.0.5", "electron-context-menu": "^4.1.0",
"electron-drag-click": "git+https://github.com/zubiden/electron-drag-click#cf6918ddb648e13ebcf6cf1e7aa008258edc06ad", "electron-drag-click": "git+https://github.com/zubiden/electron-drag-click#cf6918ddb648e13ebcf6cf1e7aa008258edc06ad",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"electronmon": "^2.0.3", "electronmon": "^2.0.3",
"eslint": "^9.26.0", "eslint": "^9.27.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0", "eslint-plugin-jest": "^28.11.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
@ -113,7 +113,7 @@
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
"postcss-modules": "^6.0.1", "postcss-modules": "^6.0.1",
"react": "^19.1.0", "react": "^19.1.0",
"sass": "^1.88.0", "sass": "^1.89.0",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"script-loader": "^0.7.2", "script-loader": "^0.7.2",
"serve": "^14.2.4", "serve": "^14.2.4",
@ -148,7 +148,7 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"dmg-license": "^1.0.11", "dmg-license": "^1.0.11",
"fsevents": "2.3.3" "fsevents": "^2.3.3"
}, },
"overrides": { "overrides": {
"bindings": "$bindings" "bindings": "$bindings"

View File

@ -20,6 +20,9 @@ export const IS_TEST = process.env.APP_ENV === 'test';
export const IS_PERF = process.env.APP_ENV === 'perf'; export const IS_PERF = process.env.APP_ENV === 'perf';
export const IS_BETA = process.env.APP_ENV === 'staging'; export const IS_BETA = process.env.APP_ENV === 'staging';
export const IS_PACKAGED_ELECTRON = process.env.IS_PACKAGED_ELECTRON; export const IS_PACKAGED_ELECTRON = process.env.IS_PACKAGED_ELECTRON;
export const ELECTRON_WINDOW_DRAG_EVENT_START = 'tt-electron-window-drag-start';
export const ELECTRON_WINDOW_DRAG_EVENT_END = 'tt-electron-window-drag-end';
export const PAID_MESSAGES_PURPOSE = 'paid_messages'; export const PAID_MESSAGES_PURPOSE = 'paid_messages';
export const DEBUG = process.env.APP_ENV !== 'production'; export const DEBUG = process.env.APP_ENV !== 'production';

86
src/electron/config.js Normal file
View File

@ -0,0 +1,86 @@
const config = {
productName: 'Telegram A',
artifactName: '${productName}-${arch}.${ext}',
appId: 'org.telegram.TelegramA',
extraMetadata: {
main: './dist/electron.cjs',
productName: 'Telegram A',
},
asarUnpack: [
'build/Release/electron_drag_click.node',
],
files: [
'dist',
'package.json',
'public/icon-electron-windows.ico',
'build/Release/electron_drag_click.node',
'!dist/**/build-stats.json',
'!dist/**/statoscope-report.html',
'!dist/**/reference.json',
'!dist/img-apple-*',
'!dist/get',
'!node_modules',
],
directories: {
buildResources: './public',
output: './dist-electron',
},
protocols: [
{
name: 'Tg',
schemes: ['tg'],
},
],
publish: {
provider: 'github',
owner: 'Ajaxy',
repo: 'telegram-tt',
releaseType: 'draft',
},
win: {
target: {
target: 'nsis',
arch: ['x64'],
},
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',
},
dmg: {
background: 'public/background-electron-dmg.tiff',
iconSize: 100,
contents: [
{
x: 138,
y: 225,
type: 'file',
},
{
x: 402,
y: 225,
type: 'link',
path: '/Applications',
},
],
},
linux: {
category: 'Chat;Network;InstantMessaging;',
target: {
target: 'AppImage',
arch: ['x64'],
},
},
};
export default config;

View File

@ -1,63 +0,0 @@
productName: Telegram A
artifactName: ${productName}-${arch}.${ext}
appId: org.telegram.TelegramA
extraMetadata:
main: ./dist/electron.cjs
productName: Telegram A
files:
- "dist"
- "package.json"
- "public/icon-electron-windows.ico"
- "!dist/**/build-stats.json"
- "!dist/**/statoscope-report.html"
- "!dist/**/reference.json"
- "!dist/img-apple-*"
- "!dist/get"
- "!node_modules"
directories:
buildResources: ./public
output: ./dist-electron
protocols:
- name: Tg
schemes:
- tg
publish:
provider: github
owner: Ajaxy
repo: telegram-tt
releaseType: draft
win:
target:
target: nsis
arch:
- x64
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
dmg:
background: public/background-electron-dmg.tiff
iconSize: 100
contents:
- x: 138
y: 225
type: file
- x: 402
y: 225
type: link
path: "/Applications"
linux:
category: Community
target:
target: AppImage
arch:
- x64

View File

@ -8,7 +8,9 @@ import { IS_MAC_OS, IS_PRODUCTION, IS_WINDOWS } from './utils';
import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window'; import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window';
initDeeplink(); initDeeplink();
electronDragClick(); if (IS_MAC_OS) {
electronDragClick();
}
contextMenu({ contextMenu({
showLearnSpelling: false, showLearnSpelling: false,

View File

@ -66,7 +66,7 @@ export function createWindow(url?: string) {
}, },
...(IS_MAC_OS && { ...(IS_MAC_OS && {
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
windowButtonPosition: WINDOW_BUTTONS_POSITION.standard, trafficLightPosition: WINDOW_BUTTONS_POSITION.standard,
}), }),
}); });

View File

@ -1,6 +1,7 @@
import type { ElementRef } from '../lib/teact/teact'; import type { ElementRef } from '../lib/teact/teact';
import { useEffect, useRef } from '../lib/teact/teact'; import { useEffect, useRef } from '../lib/teact/teact';
import { ELECTRON_WINDOW_DRAG_EVENT_END, ELECTRON_WINDOW_DRAG_EVENT_START } from '../config';
import { IS_ELECTRON, IS_MAC_OS } from '../util/browser/windowEnvironment'; import { IS_ELECTRON, IS_MAC_OS } from '../util/browser/windowEnvironment';
const DRAG_DISTANCE_THRESHOLD = 5; const DRAG_DISTANCE_THRESHOLD = 5;
@ -17,31 +18,34 @@ const useElectronDrag = (ref: ElementRef<HTMLDivElement>) => {
if (!element || !(IS_ELECTRON && IS_MAC_OS)) return undefined; if (!element || !(IS_ELECTRON && IS_MAC_OS)) return undefined;
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
distance.current = 0;
if (isDragging.current) { if (isDragging.current) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
isDragging.current = false; isDragging.current = false;
document.body.dispatchEvent(new CustomEvent(ELECTRON_WINDOW_DRAG_EVENT_END));
} }
}; };
const handleMousedown = (event: MouseEvent) => { const handleMouseDown = (event: MouseEvent) => {
if (isDragging.current) { distance.current = 0;
event.preventDefault(); isDragging.current = false;
event.stopPropagation(); x.current = window.screenX;
} y.current = window.screenY;
}; };
const handleDrag = (event: MouseEvent) => { const handleDrag = (event: MouseEvent) => {
if (event.buttons === 1) { if (event.buttons === 1) {
distance.current += Math.sqrt((x.current - window.screenX) ** 2 + (y.current - window.screenY) ** 2); const deltaX = x.current - window.screenX;
const deltaY = y.current - window.screenY;
const deltaDistance = Math.sqrt(deltaX ** 2 + deltaY ** 2);
distance.current += deltaDistance;
x.current = window.screenX; x.current = window.screenX;
y.current = window.screenY; y.current = window.screenY;
if (!isDragging.current && distance.current > DRAG_DISTANCE_THRESHOLD) { if (!isDragging.current && distance.current > DRAG_DISTANCE_THRESHOLD) {
isDragging.current = true; isDragging.current = true;
document.body.dispatchEvent(new CustomEvent(ELECTRON_WINDOW_DRAG_EVENT_START));
} }
} }
}; };
@ -53,13 +57,13 @@ const useElectronDrag = (ref: ElementRef<HTMLDivElement>) => {
}; };
element.addEventListener('click', handleClick); element.addEventListener('click', handleClick);
element.addEventListener('mousedown', handleMousedown); element.addEventListener('mousedown', handleMouseDown);
element.addEventListener('mousemove', handleDrag); element.addEventListener('mousemove', handleDrag);
element.addEventListener('dblclick', handleDoubleClick); element.addEventListener('dblclick', handleDoubleClick);
return () => { return () => {
element.removeEventListener('click', handleClick); element.removeEventListener('click', handleClick);
element.removeEventListener('mouseup', handleMousedown); element.removeEventListener('mousedown', handleMouseDown);
element.removeEventListener('mousemove', handleDrag); element.removeEventListener('mousemove', handleDrag);
element.removeEventListener('dblclick', handleDoubleClick); element.removeEventListener('dblclick', handleDoubleClick);
}; };

View File

@ -1,4 +1,7 @@
import { useCallback, useRef, useUnmountCleanup } from '../lib/teact/teact'; import { useEffect, useRef, useUnmountCleanup } from '../lib/teact/teact';
import { ELECTRON_WINDOW_DRAG_EVENT_START } from '../config';
import useLastCallback from './useLastCallback';
const DEFAULT_THRESHOLD = 250; const DEFAULT_THRESHOLD = 250;
@ -14,7 +17,7 @@ function useLongPress({
const isPressed = useRef(false); const isPressed = useRef(false);
const timerId = useRef<number | undefined>(undefined); const timerId = useRef<number | undefined>(undefined);
const start = useCallback((e: React.MouseEvent | React.TouchEvent) => { const start = useLastCallback((e: React.MouseEvent | React.TouchEvent) => {
const canProcessEvent = ('button' in e && e.button === 0) || ('touches' in e && e.touches.length > 0); const canProcessEvent = ('button' in e && e.button === 0) || ('touches' in e && e.touches.length > 0);
if (isPressed.current || !canProcessEvent) { if (isPressed.current || !canProcessEvent) {
return; return;
@ -25,9 +28,9 @@ function useLongPress({
onStart?.(); onStart?.();
isLongPressActive.current = true; isLongPressActive.current = true;
}, threshold); }, threshold);
}, [onStart, threshold]); });
const cancel = useCallback((e: React.MouseEvent | React.TouchEvent) => { const end = useLastCallback((e: React.MouseEvent | React.TouchEvent) => {
if (!isPressed.current) return; if (!isPressed.current) return;
if (isLongPressActive.current) { if (isLongPressActive.current) {
@ -36,21 +39,33 @@ function useLongPress({
onClick?.(e); onClick?.(e);
} }
cancel();
});
const cancel = useLastCallback(() => {
isLongPressActive.current = false; isLongPressActive.current = false;
isPressed.current = false; isPressed.current = false;
window.clearTimeout(timerId.current); window.clearTimeout(timerId.current);
}, [onEnd, onClick]); });
useUnmountCleanup(() => { useUnmountCleanup(() => {
window.clearTimeout(timerId.current); window.clearTimeout(timerId.current);
}); });
useEffect(() => {
document.body.addEventListener(ELECTRON_WINDOW_DRAG_EVENT_START, cancel);
return () => {
document.body.removeEventListener(ELECTRON_WINDOW_DRAG_EVENT_START, cancel);
};
}, []);
return { return {
onMouseDown: start, onMouseDown: start,
onMouseUp: cancel, onMouseUp: end,
onMouseLeave: cancel, onMouseLeave: end,
onTouchStart: start, onTouchStart: start,
onTouchEnd: cancel, onTouchEnd: end,
}; };
} }

View File

@ -1,3 +1,4 @@
import CopyWebpackPlugin from 'copy-webpack-plugin';
import path from 'path'; import path from 'path';
import { EnvironmentPlugin } from 'webpack'; import { EnvironmentPlugin } from 'webpack';
@ -36,6 +37,14 @@ export default {
BASE_URL, BASE_URL,
IS_PREVIEW: false, IS_PREVIEW: false,
}), }),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, 'node_modules/electron-drag-click/build/Release/electron_drag_click.node'),
to: path.resolve(__dirname, 'build/Release/electron_drag_click.node'),
},
],
}),
], ],
module: { module: {