Introduce Electron version (#2053)
This commit is contained in:
parent
28fc59e070
commit
82f42b6e35
71
README.md
71
README.md
@ -37,5 +37,76 @@ Example usage:
|
|||||||
await invoke(new GramJs.help.GetAppConfig())
|
await invoke(new GramJs.help.GetAppConfig())
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Electron
|
||||||
|
|
||||||
|
Electron allows building a native application that can be installed on Windows, macOS, and Linux.
|
||||||
|
|
||||||
|
#### NPM scripts
|
||||||
|
|
||||||
|
- `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).
|
||||||
|
|
||||||
|
- `npm run electron:webpack`
|
||||||
|
|
||||||
|
The main process code for Electron, which includes preload functionality, is written in TypeScript and is compiled using the `webpack-electron.config.js` configuration to generate JavaScript code.
|
||||||
|
|
||||||
|
- `npm run electron:build`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### Code signing on MacOS
|
||||||
|
|
||||||
|
To sign the code of your application, follow these steps:
|
||||||
|
|
||||||
|
- Install certificates from `/certs` folder to `login` folder of your Keychain.
|
||||||
|
- Download and install `Developer ID - G2` certificate from the [Apple PKI](https://www.apple.com/certificateauthority/) page.
|
||||||
|
- Under the Keychain application, go to the private key associated with your developer certificate. Then do `key > Get Info > Access Control`. Down there, make sure your application (Xcode) is in the list `Always allow access by these applications` and make sure `Confirm before allowing access` is turned on.
|
||||||
|
- A valid and appropriate identity from your keychain will be automatically used when you publish your application.
|
||||||
|
|
||||||
|
More info in the [official documentation](https://www.electronjs.org/docs/latest/tutorial/code-signing).
|
||||||
|
|
||||||
|
#### 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.
|
||||||
|
|
||||||
|
How to obtain app-specific password:
|
||||||
|
|
||||||
|
- Sign in to [appleid.apple.com](appleid.apple.com).
|
||||||
|
- In the "Sign-In and Security" section, select "App-Specific Passwords".
|
||||||
|
- Select "Generate an app-specific password" or select the Add button, then follow the steps on your screen.
|
||||||
|
|
||||||
|
#### GitHub release
|
||||||
|
|
||||||
|
##### GitHub access token
|
||||||
|
|
||||||
|
In order to publish new release, you need to add GitHub access token to `.env`. Generate a GitHub access token by going to https://github.com/settings/tokens/new. The access token should have the repo scope/permission. Once you have the token, assign it to an environment variable:
|
||||||
|
|
||||||
|
```
|
||||||
|
# .env
|
||||||
|
GH_TOKEN="{YOUR_TOKEN_HERE}"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Publish settings
|
||||||
|
|
||||||
|
Publish configuration in `src/electron/config.yml` config file allows to set GitHub repository owner/name.
|
||||||
|
|
||||||
|
##### Release workflow
|
||||||
|
|
||||||
|
- Run `npm run electron:publish`, which will create new draft release and upload build artefacts to newly reated release. Version of created release will be the same as in `package.json`.
|
||||||
|
- Once you are done, publish the release. GitHub will tag the latest commit.
|
||||||
|
|
||||||
## Bug reports and Suggestions
|
## Bug reports and Suggestions
|
||||||
If you find an issue with this app, let Telegram know using the [Suggestions Platform](https://bugs.telegram.org/c/4002).
|
If you find an issue with this app, let Telegram know using the [Suggestions Platform](https://bugs.telegram.org/c/4002).
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
// Comes from: https://raw.githubusercontent.com/statoscope/statoscope.tech/main/custom-ext.js
|
|
||||||
|
|
||||||
module.exports = class WebpackContextExtension {
|
|
||||||
constructor() {
|
|
||||||
this.context = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCompiler(compiler) {
|
|
||||||
this.context = compiler.context;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtension() {
|
|
||||||
return {
|
|
||||||
descriptor: { name: 'custom-webpack-extension-context', version: '1.0.0' },
|
|
||||||
payload: { context: this.context },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
12858
package-lock.json
generated
12858
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -25,6 +25,12 @@
|
|||||||
"test:record": "playwright codegen localhost:1235",
|
"test:record": "playwright codegen localhost:1235",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json",
|
"statoscope:validate-diff": "statoscope validate --input input.json --reference reference.json",
|
||||||
|
"electron:dev": "npm run electron:webpack && IS_ELECTRON=true concurrently -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron\"",
|
||||||
|
"electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts",
|
||||||
|
"electron:build": "cross-env IS_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && electron-rebuild && ENV=$ENV npm run electron:webpack",
|
||||||
|
"electron:package:staging": "ENV=staging npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.yml",
|
||||||
|
"electron:package:production": "ENV=production npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.yml",
|
||||||
|
"electron:publish": "npm run electron:package:production -- -p always",
|
||||||
"postinstall": "(cd dev/eslint-multitab && npm i)"
|
"postinstall": "(cd dev/eslint-multitab && npm i)"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -40,6 +46,9 @@
|
|||||||
"*.{ts,tsx,js}": "eslint --fix",
|
"*.{ts,tsx,js}": "eslint --fix",
|
||||||
"*.{css,scss}": "stylelint --fix"
|
"*.{css,scss}": "stylelint --fix"
|
||||||
},
|
},
|
||||||
|
"electronmon": {
|
||||||
|
"logLevel": "quiet"
|
||||||
|
},
|
||||||
"author": "Alexander Zinchuk (alexander@zinchuk.com)",
|
"author": "Alexander Zinchuk (alexander@zinchuk.com)",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -51,6 +60,8 @@
|
|||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.21.0",
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
|
"@babel/register": "^7.21.0",
|
||||||
|
"@electron/rebuild": "^3.2.10",
|
||||||
"@glen/jest-raw-loader": "^2.0.0",
|
"@glen/jest-raw-loader": "^2.0.0",
|
||||||
"@playwright/test": "^1.31.2",
|
"@playwright/test": "^1.31.2",
|
||||||
"@statoscope/cli": "^5.26.1",
|
"@statoscope/cli": "^5.26.1",
|
||||||
@ -61,6 +72,8 @@
|
|||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/webpack": "^5.28.1",
|
||||||
|
"@types/webpack-dev-server": "^4.7.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
"@typescript-eslint/parser": "^5.55.0",
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
"@webpack-cli/serve": "^2.0.1",
|
"@webpack-cli/serve": "^2.0.1",
|
||||||
@ -69,9 +82,15 @@
|
|||||||
"babel-plugin-transform-import-meta": "^2.2.0",
|
"babel-plugin-transform-import-meta": "^2.2.0",
|
||||||
"browserlist": "^1.0.1",
|
"browserlist": "^1.0.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"concurrently": "^7.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^6.7.3",
|
"css-loader": "^6.7.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
"electron": "^22.0.0",
|
||||||
|
"electron-builder": "^23.6.0",
|
||||||
|
"electron-updater": "^5.3.0",
|
||||||
|
"electron-window-state": "^5.0.3",
|
||||||
|
"electronmon": "^2.0.2",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
@ -129,7 +148,8 @@
|
|||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"qr-code-styling": "github:zubiden/qr-code-styling#dbbfed0"
|
"qr-code-styling": "github:zubiden/qr-code-styling#dbbfed0",
|
||||||
|
"v8-compile-cache": "^2.3.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
|
|||||||
12
public/electron-entitlements.mac.plist
Normal file
12
public/electron-entitlements.mac.plist
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!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>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
public/icon-electron-macos.icns
Normal file
BIN
public/icon-electron-macos.icns
Normal file
Binary file not shown.
BIN
public/icon-electron-macos.png
Normal file
BIN
public/icon-electron-macos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/icon-electron-windows.ico
Normal file
BIN
public/icon-electron-windows.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -59,6 +59,14 @@
|
|||||||
margin-bottom: 3rem;
|
margin-bottom: 3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-electron #auth-phone-number-form & {
|
||||||
|
padding-top: 3rem;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#auth-registration-form,
|
#auth-registration-form,
|
||||||
@ -68,10 +76,18 @@
|
|||||||
#auth-qr-form {
|
#auth-qr-form {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
body.is-electron.is-macos & {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#auth-phone-number-form {
|
#auth-phone-number-form {
|
||||||
form {
|
.form {
|
||||||
min-height: 26.25rem;
|
min-height: 26.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { GlobalState } from '../../global/types';
|
import type { GlobalState } from '../../global/types';
|
||||||
@ -8,6 +8,7 @@ import '../../global/actions/initial';
|
|||||||
import { PLATFORM_ENV } from '../../util/windowEnvironment';
|
import { PLATFORM_ENV } from '../../util/windowEnvironment';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
|
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||||
|
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
import AuthPhoneNumber from './AuthPhoneNumber';
|
import AuthPhoneNumber from './AuthPhoneNumber';
|
||||||
@ -43,6 +44,10 @@ const Auth: FC<StateProps> = ({
|
|||||||
onBack: handleChangeAuthorizationMethod,
|
onBack: handleChangeAuthorizationMethod,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useElectronDrag(containerRef);
|
||||||
|
|
||||||
// For animation purposes
|
// For animation purposes
|
||||||
const renderingAuthState = useCurrentOrPrev(
|
const renderingAuthState = useCurrentOrPrev(
|
||||||
authState !== 'authorizationStateReady' ? authState : undefined,
|
authState !== 'authorizationStateReady' ? authState : undefined,
|
||||||
@ -84,7 +89,7 @@ const Auth: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition activeKey={getActiveKey()} name="fade" className="Auth">
|
<Transition activeKey={getActiveKey()} name="fade" className="Auth" ref={containerRef}>
|
||||||
{getScreen()}
|
{getScreen()}
|
||||||
</Transition>
|
</Transition>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -216,7 +216,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
|||||||
<div id="logo" />
|
<div id="logo" />
|
||||||
<h1>Telegram</h1>
|
<h1>Telegram</h1>
|
||||||
<p className="note">{lang('StartText')}</p>
|
<p className="note">{lang('StartText')}</p>
|
||||||
<form action="" onSubmit={handleSubmit}>
|
<form className="form" action="" onSubmit={handleSubmit}>
|
||||||
<CountryCodeInput
|
<CountryCodeInput
|
||||||
id="sign-in-phone-code"
|
id="sign-in-phone-code"
|
||||||
value={country}
|
value={country}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import type { TeactNode } from '../../../lib/teact/teact';
|
|||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
|
|
||||||
import EMOJI_REGEX from '../../../lib/twemojiRegex';
|
import EMOJI_REGEX from '../../../lib/twemojiRegex';
|
||||||
import { RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE } from '../../../config';
|
import {
|
||||||
|
RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE, IS_ELECTRON, PRODUCTION_URL,
|
||||||
|
} from '../../../config';
|
||||||
import { IS_EMOJI_SUPPORTED } from '../../../util/windowEnvironment';
|
import { IS_EMOJI_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
fixNonStandardEmoji,
|
fixNonStandardEmoji,
|
||||||
@ -108,7 +110,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
|||||||
return emojis.reduce((emojiResult: TextPart[], emoji, i) => {
|
return emojis.reduce((emojiResult: TextPart[], emoji, i) => {
|
||||||
const code = nativeToUnifiedExtendedWithCache(emoji);
|
const code = nativeToUnifiedExtendedWithCache(emoji);
|
||||||
if (!code) return emojiResult;
|
if (!code) return emojiResult;
|
||||||
const src = `./img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
|
const src = `${IS_ELECTRON ? PRODUCTION_URL : '.'}/img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'emoji',
|
'emoji',
|
||||||
size === 'small' && 'emoji-small',
|
size === 'small' && 'emoji-small',
|
||||||
@ -132,7 +134,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
|||||||
`<img\
|
`<img\
|
||||||
draggable="false"\
|
draggable="false"\
|
||||||
class="${className}"\
|
class="${className}"\
|
||||||
src="./img-apple-${size === 'big' ? '160' : '64'}/${code}.png"\
|
src={src}\
|
||||||
alt="${emoji}"\
|
alt="${emoji}"\
|
||||||
/>`,
|
/>`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -40,4 +40,27 @@
|
|||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos & {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
.SearchInput {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos #Main:not(.is-fullscreen) &:not(#TopicListHeader) {
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 4.5rem;
|
||||||
|
|
||||||
|
.SearchInput {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
max-width: calc(100% - 2.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Menu.main-menu .bubble {
|
||||||
|
--offset-y: 100%;
|
||||||
|
--offset-x: -4.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { LeftColumnContent, SettingsScreens } from '../../types';
|
|||||||
import type { ReducerAction } from '../../hooks/useReducer';
|
import type { ReducerAction } from '../../hooks/useReducer';
|
||||||
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
|
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
|
||||||
|
|
||||||
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/windowEnvironment';
|
import { IS_MAC_OS, IS_APP, LAYERS_ANIMATION_NAME } from '../../util/windowEnvironment';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
import { selectCurrentChat, selectIsForumPanelOpen, selectTabState } from '../../global/selectors';
|
import { selectCurrentChat, selectIsForumPanelOpen, selectTabState } from '../../global/selectors';
|
||||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||||
@ -381,11 +381,11 @@ function LeftColumn({
|
|||||||
useHotkeys({
|
useHotkeys({
|
||||||
'Mod+Shift+F': handleHotkeySearch,
|
'Mod+Shift+F': handleHotkeySearch,
|
||||||
'Mod+Shift+S': handleHotkeySavedMessages,
|
'Mod+Shift+S': handleHotkeySavedMessages,
|
||||||
...(IS_PWA && {
|
...(IS_APP && {
|
||||||
'Mod+0': handleHotkeySavedMessages,
|
'Mod+0': handleHotkeySavedMessages,
|
||||||
'Mod+9': handleArchivedChats,
|
'Mod+9': handleArchivedChats,
|
||||||
}),
|
}),
|
||||||
...(IS_MAC_OS && IS_PWA && { 'Mod+,': handleHotkeySettings }),
|
...(IS_MAC_OS && IS_APP && { 'Mod+,': handleHotkeySettings }),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
CHAT_HEIGHT_PX,
|
CHAT_HEIGHT_PX,
|
||||||
CHAT_LIST_SLICE,
|
CHAT_LIST_SLICE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_MAC_OS, IS_PWA } from '../../../util/windowEnvironment';
|
import { IS_MAC_OS, IS_APP } from '../../../util/windowEnvironment';
|
||||||
import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager';
|
import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
|
|
||||||
// Support <Cmd>+<Digit> to navigate between chats
|
// Support <Cmd>+<Digit> to navigate between chats
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive || !orderedIds || !IS_PWA) {
|
if (!isActive || !orderedIds || !IS_APP) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -217,7 +217,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
onTransitionEnd={!isOpen ? onCloseAnimationEnd : undefined}
|
onTransitionEnd={!isOpen ? onCloseAnimationEnd : undefined}
|
||||||
>
|
>
|
||||||
<div className="left-header">
|
<div id="TopicListHeader" className="left-header">
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="smaller"
|
size="smaller"
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { SettingsScreens } from '../../../types';
|
|||||||
import { LeftColumnContent } from '../../../types';
|
import { LeftColumnContent } from '../../../types';
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
|
|
||||||
|
import { IS_ELECTRON } from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
@ -117,7 +118,11 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
}, [closeForumPanel, onContentChange]);
|
}, [closeForumPanel, onContentChange]);
|
||||||
|
|
||||||
const handleUpdateClick = useCallback(() => {
|
const handleUpdateClick = useCallback(() => {
|
||||||
window.location.reload();
|
if (IS_ELECTRON) {
|
||||||
|
window.electron?.installUpdate();
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectNewChannel = useCallback(() => {
|
const handleSelectNewChannel = useCallback(() => {
|
||||||
|
|||||||
@ -102,6 +102,10 @@
|
|||||||
.extra-spacing {
|
.extra-spacing {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: 0.8125rem;
|
margin-left: 0.8125rem;
|
||||||
|
|
||||||
|
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-status-effect {
|
.emoji-status-effect {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
@ -18,14 +18,15 @@ import {
|
|||||||
FEEDBACK_URL,
|
FEEDBACK_URL,
|
||||||
IS_BETA,
|
IS_BETA,
|
||||||
IS_TEST,
|
IS_TEST,
|
||||||
|
IS_ELECTRON,
|
||||||
PRODUCTION_HOSTNAME,
|
PRODUCTION_HOSTNAME,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
|
import { IS_APP } from '../../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
INITIAL_PERFORMANCE_STATE_MAX,
|
INITIAL_PERFORMANCE_STATE_MAX,
|
||||||
INITIAL_PERFORMANCE_STATE_MID,
|
INITIAL_PERFORMANCE_STATE_MID,
|
||||||
INITIAL_PERFORMANCE_STATE_MIN,
|
INITIAL_PERFORMANCE_STATE_MIN,
|
||||||
} from '../../../global/initialState';
|
} from '../../../global/initialState';
|
||||||
import { IS_PWA } from '../../../util/windowEnvironment';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatDateToString } from '../../../util/dateFormat';
|
import { formatDateToString } from '../../../util/dateFormat';
|
||||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||||
@ -40,6 +41,8 @@ import { useHotkeys } from '../../../hooks/useHotkeys';
|
|||||||
import { getPromptInstall } from '../../../util/installPrompt';
|
import { getPromptInstall } from '../../../util/installPrompt';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
||||||
|
import { useFullscreenStatus } from '../../../hooks/useFullscreen';
|
||||||
|
import useElectronDrag from '../../../hooks/useElectronDrag';
|
||||||
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
@ -163,7 +166,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
'Ctrl+Shift+L': handleLockScreenHotkey,
|
'Ctrl+Shift+L': handleLockScreenHotkey,
|
||||||
'Alt+Shift+L': handleLockScreenHotkey,
|
'Alt+Shift+L': handleLockScreenHotkey,
|
||||||
'Meta+Shift+L': handleLockScreenHotkey,
|
'Meta+Shift+L': handleLockScreenHotkey,
|
||||||
...(IS_PWA && { 'Mod+L': handleLockScreenHotkey }),
|
...(IS_APP && { 'Mod+L': handleLockScreenHotkey }),
|
||||||
} : undefined);
|
} : undefined);
|
||||||
|
|
||||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
||||||
@ -266,12 +269,18 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
? (animationLevel === ANIMATION_LEVEL_MAX ? 'max' : 'mid')
|
? (animationLevel === ANIMATION_LEVEL_MAX ? 'max' : 'mid')
|
||||||
: 'min';
|
: 'min';
|
||||||
|
|
||||||
|
const isFullscreen = useFullscreenStatus();
|
||||||
|
|
||||||
// Disable dropdown menu RTL animation for resize
|
// Disable dropdown menu RTL animation for resize
|
||||||
const {
|
const {
|
||||||
shouldDisableDropdownMenuTransitionRef,
|
shouldDisableDropdownMenuTransitionRef,
|
||||||
handleDropdownMenuTransitionEnd,
|
handleDropdownMenuTransitionEnd,
|
||||||
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
|
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useElectronDrag(headerRef);
|
||||||
|
|
||||||
const menuItems = useMemo(() => (
|
const menuItems = useMemo(() => (
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -369,17 +378,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LeftMainHeader">
|
<div className="LeftMainHeader">
|
||||||
<div id="LeftMainHeader" className="left-header">
|
<div id="LeftMainHeader" className="left-header" ref={headerRef}>
|
||||||
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
trigger={MainButton}
|
trigger={MainButton}
|
||||||
footer={`${APP_NAME} ${versionString}`}
|
footer={`${APP_NAME} ${versionString}`}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
|
'main-menu',
|
||||||
lang.isRtl && 'rtl',
|
lang.isRtl && 'rtl',
|
||||||
shouldHideSearch && lang.isRtl && 'right-aligned',
|
shouldHideSearch && lang.isRtl && 'right-aligned',
|
||||||
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
|
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
|
||||||
)}
|
)}
|
||||||
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
|
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
|
||||||
|
transformOriginX={IS_ELECTRON && !isFullscreen ? 90 : undefined}
|
||||||
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
|
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
|
||||||
>
|
>
|
||||||
{menuItems}
|
{menuItems}
|
||||||
|
|||||||
@ -15,10 +15,11 @@ import type {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||||
|
import { ElectronEvent } from '../../types/electron';
|
||||||
|
|
||||||
import '../../global/actions/all';
|
import '../../global/actions/all';
|
||||||
import {
|
import {
|
||||||
BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER,
|
BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, IS_ELECTRON,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { IS_ANDROID } from '../../util/windowEnvironment';
|
import { IS_ANDROID } from '../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
@ -52,6 +53,7 @@ import useForceUpdate from '../../hooks/useForceUpdate';
|
|||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
import useInterval from '../../hooks/useInterval';
|
import useInterval from '../../hooks/useInterval';
|
||||||
|
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useTimeout from '../../hooks/useTimeout';
|
import useTimeout from '../../hooks/useTimeout';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
@ -239,6 +241,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadTopReactions,
|
loadTopReactions,
|
||||||
loadRecentReactions,
|
loadRecentReactions,
|
||||||
loadFeaturedEmojiStickers,
|
loadFeaturedEmojiStickers,
|
||||||
|
setIsAppUpdateAvailable,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -271,7 +274,27 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, isMobile, toggleLeftColumn]);
|
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, isMobile, toggleLeftColumn]);
|
||||||
|
|
||||||
useInterval(checkAppVersion, isMasterTab ? APP_OUTDATED_TIMEOUT_MS : undefined, true);
|
useInterval(checkAppVersion, (isMasterTab && !IS_ELECTRON) ? APP_OUTDATED_TIMEOUT_MS : undefined, true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_ELECTRON) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUpdateDownloadedListener = window.electron?.on(ElectronEvent.UPDATE_DOWNLOADED, () => {
|
||||||
|
setIsAppUpdateAvailable(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeUpdateErrorListener = window.electron?.on(ElectronEvent.UPDATE_ERROR, () => {
|
||||||
|
setIsAppUpdateAvailable(false);
|
||||||
|
removeUpdateDownloadedListener?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeUpdateErrorListener?.();
|
||||||
|
removeUpdateDownloadedListener?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initial API calls
|
// Initial API calls
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -427,6 +450,8 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
const willAnimateRightColumnRef = useRef(false);
|
const willAnimateRightColumnRef = useRef(false);
|
||||||
const [isNarrowMessageList, setIsNarrowMessageList] = useState(isRightColumnOpen);
|
const [isNarrowMessageList, setIsNarrowMessageList] = useState(isRightColumnOpen);
|
||||||
|
|
||||||
|
const isFullscreen = useFullscreenStatus();
|
||||||
|
|
||||||
// Handle opening right column
|
// Handle opening right column
|
||||||
useSyncEffect(([prevIsRightColumnOpen]) => {
|
useSyncEffect(([prevIsRightColumnOpen]) => {
|
||||||
if (prevIsRightColumnOpen === undefined || isRightColumnOpen === prevIsRightColumnOpen) {
|
if (prevIsRightColumnOpen === undefined || isRightColumnOpen === prevIsRightColumnOpen) {
|
||||||
@ -459,6 +484,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
willAnimateRightColumnRef.current && 'right-column-animating',
|
willAnimateRightColumnRef.current && 'right-column-animating',
|
||||||
isNarrowMessageList && 'narrow-message-list',
|
isNarrowMessageList && 'narrow-message-list',
|
||||||
shouldSkipHistoryAnimations && 'history-animation-disabled',
|
shouldSkipHistoryAnimations && 'history-animation-disabled',
|
||||||
|
isFullscreen && 'is-fullscreen',
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
|
|||||||
@ -97,6 +97,14 @@
|
|||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos & {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
|
||||||
|
padding-left: 5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .Transition,
|
& > .Transition,
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import { exitPictureInPictureIfNeeded, usePictureInPictureSignal } from '../../h
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import { useMediaProps } from './hooks/useMediaProps';
|
import { useMediaProps } from './hooks/useMediaProps';
|
||||||
|
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import { useStateRef } from '../../hooks/useStateRef';
|
import { useStateRef } from '../../hooks/useStateRef';
|
||||||
|
|
||||||
@ -182,6 +183,10 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isMobile, isOpen]);
|
}, [isMobile, isOpen]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useElectronDrag(headerRef);
|
||||||
|
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
|
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
|
||||||
@ -325,7 +330,7 @@ const MediaViewer: FC<StateProps> = ({
|
|||||||
shouldAnimateFirstRender
|
shouldAnimateFirstRender
|
||||||
noCloseTransition={shouldSkipHistoryAnimations}
|
noCloseTransition={shouldSkipHistoryAnimations}
|
||||||
>
|
>
|
||||||
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined} ref={headerRef}>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Button
|
<Button
|
||||||
className="media-viewer-close"
|
className="media-viewer-close"
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { MAIN_THREAD_ID } from '../../api/types';
|
|||||||
import type { IAnchorPosition } from '../../types';
|
import type { IAnchorPosition } from '../../types';
|
||||||
import { ManagementScreens } from '../../types';
|
import { ManagementScreens } from '../../types';
|
||||||
|
|
||||||
import { ARE_CALLS_SUPPORTED, IS_PWA } from '../../util/windowEnvironment';
|
import { ARE_CALLS_SUPPORTED, IS_APP } from '../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
|
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
@ -174,7 +174,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleHotkeySearchClick = useCallback((e: KeyboardEvent) => {
|
const handleHotkeySearchClick = useCallback((e: KeyboardEvent) => {
|
||||||
if (!canSearch || !IS_PWA || e.shiftKey) {
|
if (!canSearch || !IS_APP || e.shiftKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -374,6 +374,20 @@
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@include mobile-header-styles();
|
@include mobile-header-styles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos & {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos #Main:not(.left-column-open):not(.is-fullscreen) & {
|
||||||
|
@media (max-width: 925px) {
|
||||||
|
padding-left: 5rem;
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
margin-left: -0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in {
|
@keyframes fade-in {
|
||||||
|
|||||||
@ -52,6 +52,7 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useDerivedState from '../../hooks/useDerivedState';
|
import useDerivedState from '../../hooks/useDerivedState';
|
||||||
|
|
||||||
@ -409,6 +410,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|
||||||
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
|
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
|
||||||
|
|
||||||
|
useElectronDrag(componentRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="MiddleHeader" ref={componentRef}>
|
<div className="MiddleHeader" ref={componentRef}>
|
||||||
<Transition
|
<Transition
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact';
|
|||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { IS_ELECTRON, PRODUCTION_URL } from '../../../config';
|
||||||
import { IS_EMOJI_SUPPORTED } from '../../../util/windowEnvironment';
|
import { IS_EMOJI_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji';
|
import { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -29,7 +30,7 @@ const EmojiButton: FC<OwnProps> = ({
|
|||||||
focus && 'focus',
|
focus && 'focus',
|
||||||
);
|
);
|
||||||
|
|
||||||
const src = `./img-apple-64/${emoji.image}.png`;
|
const src = `${IS_ELECTRON ? PRODUCTION_URL : '.'}/img-apple-64/${emoji.image}.png`;
|
||||||
const isLoaded = LOADED_EMOJIS.has(src);
|
const isLoaded = LOADED_EMOJIS.has(src);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export type OwnProps = {
|
|||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
album?: IAlbum;
|
album?: IAlbum;
|
||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
|
targetHref?: string;
|
||||||
messageListType: MessageListType;
|
messageListType: MessageListType;
|
||||||
noReplies?: boolean;
|
noReplies?: boolean;
|
||||||
detectedLanguage?: string;
|
detectedLanguage?: string;
|
||||||
@ -121,6 +122,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
customEmojiSets,
|
customEmojiSets,
|
||||||
album,
|
album,
|
||||||
anchor,
|
anchor,
|
||||||
|
targetHref,
|
||||||
noOptions,
|
noOptions,
|
||||||
canSendNow,
|
canSendNow,
|
||||||
hasFullInfo,
|
hasFullInfo,
|
||||||
@ -480,6 +482,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
enabledReactions={enabledReactions}
|
enabledReactions={enabledReactions}
|
||||||
maxUniqueReactions={maxUniqueReactions}
|
maxUniqueReactions={maxUniqueReactions}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
|
targetHref={targetHref}
|
||||||
canShowReactionsCount={canShowReactionsCount}
|
canShowReactionsCount={canShowReactionsCount}
|
||||||
canShowReactionList={canShowReactionList}
|
canShowReactionList={canShowReactionList}
|
||||||
canSendNow={canSendNow}
|
canSendNow={canSendNow}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { AudioOrigin } from '../../../types';
|
|||||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config';
|
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID, IS_ELECTRON } from '../../../config';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
@ -394,11 +394,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
isContextMenuOpen,
|
isContextMenuOpen,
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
|
contextMenuTarget,
|
||||||
handleBeforeContextMenu,
|
handleBeforeContextMenu,
|
||||||
handleContextMenu: onContextMenu,
|
handleContextMenu: onContextMenu,
|
||||||
handleContextMenuClose,
|
handleContextMenuClose,
|
||||||
handleContextMenuHide,
|
handleContextMenuHide,
|
||||||
} = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, true, IS_ANDROID);
|
} = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, !IS_ELECTRON, IS_ANDROID);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isContextMenuOpen) {
|
if (isContextMenuOpen) {
|
||||||
@ -1324,6 +1325,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
<ContextMenuContainer
|
<ContextMenuContainer
|
||||||
isOpen={isContextMenuOpen}
|
isOpen={isContextMenuOpen}
|
||||||
anchor={contextMenuPosition}
|
anchor={contextMenuPosition}
|
||||||
|
targetHref={contextMenuTarget?.matches('a[href]') ? (contextMenuTarget as HTMLAnchorElement).href : undefined}
|
||||||
message={message}
|
message={message}
|
||||||
album={album}
|
album={album}
|
||||||
chatUsername={chatUsername?.username}
|
chatUsername={chatUsername?.username}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type OwnProps = {
|
|||||||
topReactions?: ApiReaction[];
|
topReactions?: ApiReaction[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
|
targetHref?: string;
|
||||||
message: ApiMessage | ApiSponsoredMessage;
|
message: ApiMessage | ApiSponsoredMessage;
|
||||||
canSendNow?: boolean;
|
canSendNow?: boolean;
|
||||||
enabledReactions?: ApiChatReactions;
|
enabledReactions?: ApiChatReactions;
|
||||||
@ -129,6 +130,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
enabledReactions,
|
enabledReactions,
|
||||||
maxUniqueReactions,
|
maxUniqueReactions,
|
||||||
anchor,
|
anchor,
|
||||||
|
targetHref,
|
||||||
canSendNow,
|
canSendNow,
|
||||||
canReschedule,
|
canReschedule,
|
||||||
canBuyPremium,
|
canBuyPremium,
|
||||||
@ -238,7 +240,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
const copyOptions = isSponsoredMessage
|
const copyOptions = isSponsoredMessage
|
||||||
? []
|
? []
|
||||||
: getMessageCopyOptions(
|
: getMessageCopyOptions(
|
||||||
message, handleAfterCopy, canCopyLink ? onCopyLink : undefined, onCopyMessages, onCopyNumber,
|
message, targetHref, handleAfterCopy, canCopyLink ? onCopyLink : undefined, onCopyMessages, onCopyNumber,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => {
|
const getTriggerElement = useCallback(() => {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
CLIPBOARD_ITEM_SUPPORTED,
|
CLIPBOARD_ITEM_SUPPORTED,
|
||||||
copyHtmlToClipboard,
|
copyHtmlToClipboard,
|
||||||
copyImageToClipboard,
|
copyImageToClipboard,
|
||||||
|
copyTextToClipboard,
|
||||||
} from '../../../../util/clipboard';
|
} from '../../../../util/clipboard';
|
||||||
import getMessageIdsForSelectedText from '../../../../util/getMessageIdsForSelectedText';
|
import getMessageIdsForSelectedText from '../../../../util/getMessageIdsForSelectedText';
|
||||||
import { renderMessageText } from '../../../common/helpers/renderMessageText';
|
import { renderMessageText } from '../../../common/helpers/renderMessageText';
|
||||||
@ -28,6 +29,7 @@ type ICopyOptions = {
|
|||||||
|
|
||||||
export function getMessageCopyOptions(
|
export function getMessageCopyOptions(
|
||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
|
href?: string,
|
||||||
afterEffect?: () => void,
|
afterEffect?: () => void,
|
||||||
onCopyLink?: () => void,
|
onCopyLink?: () => void,
|
||||||
onCopyMessages?: (messageIds: number[]) => void,
|
onCopyMessages?: (messageIds: number[]) => void,
|
||||||
@ -55,7 +57,17 @@ export function getMessageCopyOptions(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text) {
|
if (href) {
|
||||||
|
options.push({
|
||||||
|
label: 'lng_context_copy_link',
|
||||||
|
icon: 'copy',
|
||||||
|
handler: () => {
|
||||||
|
copyTextToClipboard(href);
|
||||||
|
|
||||||
|
afterEffect?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (text) {
|
||||||
// Detect if the user has selection in the current message
|
// Detect if the user has selection in the current message
|
||||||
const hasSelection = Boolean((
|
const hasSelection = Boolean((
|
||||||
selection?.anchorNode?.parentNode
|
selection?.anchorNode?.parentNode
|
||||||
|
|||||||
@ -46,4 +46,10 @@
|
|||||||
.DropdownMenu {
|
.DropdownMenu {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
padding-left: 5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useState,
|
memo, useCallback, useEffect, useState, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -28,6 +28,7 @@ import { getDayStartAt } from '../../util/dateFormat';
|
|||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
|
||||||
import SearchInput from '../ui/SearchInput';
|
import SearchInput from '../ui/SearchInput';
|
||||||
@ -515,8 +516,12 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
(shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
|
(shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useElectronDrag(headerRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="RightHeader">
|
<div className="RightHeader" ref={headerRef}>
|
||||||
<Button
|
<Button
|
||||||
className="close-button"
|
className="close-button"
|
||||||
round
|
round
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import './DropdownMenu.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>;
|
trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>;
|
||||||
|
transformOriginX?: number;
|
||||||
|
transformOriginY?: number;
|
||||||
positionX?: 'left' | 'right';
|
positionX?: 'left' | 'right';
|
||||||
positionY?: 'top' | 'bottom';
|
positionY?: 'top' | 'bottom';
|
||||||
footer?: string;
|
footer?: string;
|
||||||
@ -28,6 +30,8 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
trigger,
|
trigger,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
transformOriginX,
|
||||||
|
transformOriginY,
|
||||||
positionX = 'left',
|
positionX = 'left',
|
||||||
positionY = 'top',
|
positionY = 'top',
|
||||||
footer,
|
footer,
|
||||||
@ -104,6 +108,8 @@ const DropdownMenu: FC<OwnProps> = ({
|
|||||||
containerRef={dropdownRef}
|
containerRef={dropdownRef}
|
||||||
isOpen={isOpen || Boolean(forceOpen)}
|
isOpen={isOpen || Boolean(forceOpen)}
|
||||||
className={className || ''}
|
className={className || ''}
|
||||||
|
transformOriginX={transformOriginX}
|
||||||
|
transformOriginY={transformOriginY}
|
||||||
positionX={positionX}
|
positionX={positionX}
|
||||||
positionY={positionY}
|
positionY={positionY}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
|
|||||||
@ -5,17 +5,20 @@ export const APP_VERSION = process.env.APP_VERSION!;
|
|||||||
export const RELEASE_DATETIME = process.env.RELEASE_DATETIME;
|
export const RELEASE_DATETIME = process.env.RELEASE_DATETIME;
|
||||||
|
|
||||||
export const PRODUCTION_HOSTNAME = 'web.telegram.org';
|
export const PRODUCTION_HOSTNAME = 'web.telegram.org';
|
||||||
|
export const PRODUCTION_URL = 'https://web.telegram.org/a';
|
||||||
export const DEBUG = process.env.APP_ENV !== 'production';
|
|
||||||
export const DEBUG_MORE = false;
|
|
||||||
export const STRICTERDOM_ENABLED = DEBUG;
|
|
||||||
|
|
||||||
export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1';
|
export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1';
|
||||||
export const IS_TEST = process.env.APP_ENV === 'test';
|
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_ELECTRON = process.env.IS_ELECTRON;
|
||||||
|
|
||||||
|
export const DEBUG = process.env.APP_ENV !== 'production';
|
||||||
|
export const DEBUG_MORE = false;
|
||||||
|
export const STRICTERDOM_ENABLED = DEBUG && !IS_ELECTRON;
|
||||||
|
|
||||||
export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
|
export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
|
||||||
|
export const ELECTRON_HOST_URL = 'https://telegram-a-host';
|
||||||
|
|
||||||
export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.';
|
export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.';
|
||||||
export const DEBUG_GRAMJS = false;
|
export const DEBUG_GRAMJS = false;
|
||||||
|
|||||||
33
src/electron/config.yml
Normal file
33
src/electron/config.yml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
productName: "Telegram A"
|
||||||
|
artifactName: "${productName}-${arch}-${version}.${ext}"
|
||||||
|
appId: "org.telegram.TelegramA"
|
||||||
|
extraMetadata:
|
||||||
|
main: "./dist/electron.js"
|
||||||
|
files:
|
||||||
|
- "dist"
|
||||||
|
- "package.json"
|
||||||
|
- "!dist/**/build-stats.json"
|
||||||
|
- "!dist/**/statoscope-report.html"
|
||||||
|
- "!dist/**/reference.json"
|
||||||
|
- "!dist/img-apple-*"
|
||||||
|
- "!dist/libav-*"
|
||||||
|
- "!node_modules"
|
||||||
|
directories:
|
||||||
|
buildResources: "./public"
|
||||||
|
output: "./dist-electron"
|
||||||
|
publish:
|
||||||
|
provider: "github"
|
||||||
|
owner: "Ajaxy"
|
||||||
|
repo: "telegram-tt"
|
||||||
|
win:
|
||||||
|
target: "nsis"
|
||||||
|
icon: "public/icon-electron-windows.ico"
|
||||||
|
mac:
|
||||||
|
target:
|
||||||
|
target: "default"
|
||||||
|
arch: ["x64", "arm64"]
|
||||||
|
entitlements: "public/electron-entitlements.mac.plist"
|
||||||
|
icon: "public/icon-electron-macos.icns"
|
||||||
|
linux:
|
||||||
|
category: "Community"
|
||||||
|
target: ["AppImage"]
|
||||||
21
src/electron/main.ts
Normal file
21
src/electron/main.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import 'v8-compile-cache';
|
||||||
|
|
||||||
|
import { app, nativeImage } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window';
|
||||||
|
import { IS_MAC_OS, IS_WINDOWS } from './utils';
|
||||||
|
|
||||||
|
app.on('ready', () => {
|
||||||
|
if (IS_MAC_OS) {
|
||||||
|
app.dock.setIcon(nativeImage.createFromPath(path.resolve(__dirname, '../public/icon-electron-macos.png')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
app.setAppUserModelId(app.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
createWindow();
|
||||||
|
setupElectronActionHandlers();
|
||||||
|
setupCloseHandlers();
|
||||||
|
});
|
||||||
24
src/electron/preload.ts
Normal file
24
src/electron/preload.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
import type { IpcRendererEvent } from 'electron';
|
||||||
|
import type { ElectronApi, ElectronEvent } from '../types/electron';
|
||||||
|
import { ElectronAction } from '../types/electron';
|
||||||
|
|
||||||
|
const electronApi: ElectronApi = {
|
||||||
|
isFullscreen: () => ipcRenderer.invoke(ElectronAction.GET_IS_FULLSCREEN),
|
||||||
|
installUpdate: () => ipcRenderer.invoke(ElectronAction.INSTALL_UPDATE),
|
||||||
|
handleDoubleClick: () => ipcRenderer.invoke(ElectronAction.HANDLE_DOUBLE_CLICK),
|
||||||
|
openNewWindow: (url: string) => ipcRenderer.invoke(ElectronAction.OPEN_NEW_WINDOW, url),
|
||||||
|
|
||||||
|
on: (eventName: ElectronEvent, callback) => {
|
||||||
|
const subscription = (event: IpcRendererEvent, ...args: any) => callback(...args);
|
||||||
|
|
||||||
|
ipcRenderer.on(eventName, subscription);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(eventName, subscription);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronApi);
|
||||||
3
src/electron/utils.ts
Normal file
3
src/electron/utils.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const IS_MAC_OS = process.platform === 'darwin';
|
||||||
|
export const IS_WINDOWS = process.platform === 'win32';
|
||||||
|
export const IS_LINUX = process.platform === 'linux';
|
||||||
203
src/electron/window.ts
Normal file
203
src/electron/window.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
let forceQuit = false;
|
||||||
|
let interval: NodeJS.Timer;
|
||||||
|
|
||||||
|
const windows = new Set<BrowserWindow>();
|
||||||
|
const CHECK_UPDATE_INTERVAL = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
export function createWindow(url?: string) {
|
||||||
|
const windowState = windowStateKeeper({
|
||||||
|
defaultWidth: 1088,
|
||||||
|
defaultHeight: IS_MAC_OS ? 700 : 750,
|
||||||
|
});
|
||||||
|
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
if (currentWindow) {
|
||||||
|
const [currentWindowX, currentWindowY] = currentWindow.getPosition();
|
||||||
|
x = currentWindowX + 24;
|
||||||
|
y = currentWindowY + 24;
|
||||||
|
} else {
|
||||||
|
x = windowState.x;
|
||||||
|
y = windowState.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width;
|
||||||
|
let height;
|
||||||
|
|
||||||
|
if (currentWindow) {
|
||||||
|
const bounds = currentWindow.getBounds();
|
||||||
|
|
||||||
|
width = bounds.width;
|
||||||
|
height = bounds.height;
|
||||||
|
} else {
|
||||||
|
width = windowState.width;
|
||||||
|
height = windowState.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
minWidth: 360,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
title: 'Telegram A',
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
devTools: process.env.APP_ENV !== 'production',
|
||||||
|
},
|
||||||
|
...(IS_MAC_OS && {
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
trafficLightPosition: { x: 10, y: 20 },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on('page-title-updated', (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
windowState.manage(window);
|
||||||
|
|
||||||
|
window.webContents.setWindowOpenHandler((details: HandlerDetails) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on('enter-full-screen', () => {
|
||||||
|
window.webContents.send(ElectronEvent.FULLSCREEN_CHANGE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on('leave-full-screen', () => {
|
||||||
|
window.webContents.send(ElectronEvent.FULLSCREEN_CHANGE, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on('close', (event) => {
|
||||||
|
if (IS_MAC_OS) {
|
||||||
|
if (forceQuit) {
|
||||||
|
app.exit(0);
|
||||||
|
forceQuit = false;
|
||||||
|
} else {
|
||||||
|
const hasExtraWindows = BrowserWindow.getAllWindows().length > 1;
|
||||||
|
|
||||||
|
if (hasExtraWindows) {
|
||||||
|
windows.delete(window);
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
window.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
window.loadURL(url);
|
||||||
|
} else if (app.isPackaged) {
|
||||||
|
window.loadURL(`file://${__dirname}/index.html`);
|
||||||
|
} else {
|
||||||
|
window.loadURL('http://localhost:1234');
|
||||||
|
window.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IS_MAC_OS) {
|
||||||
|
window.removeMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.webContents.once('dom-ready', () => {
|
||||||
|
window.show();
|
||||||
|
setupAutoUpdates(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => window.webContents.send(ElectronEvent.UPDATE_ERROR, error));
|
||||||
|
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||||
|
window.webContents.send(ElectronEvent.UPDATE_DOWNLOADED, info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupElectronActionHandlers() {
|
||||||
|
ipcMain.handle(ElectronAction.OPEN_NEW_WINDOW, (_, newWindowUrl: string) => {
|
||||||
|
createWindow(newWindowUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(ElectronAction.GET_IS_FULLSCREEN, () => {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
currentWindow?.isFullScreen();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(ElectronAction.HANDLE_DOUBLE_CLICK, () => {
|
||||||
|
const currentWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
const doubleClickAction = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||||
|
|
||||||
|
if (doubleClickAction === 'Minimize') {
|
||||||
|
currentWindow?.minimize();
|
||||||
|
} else if (doubleClickAction === 'Maximize') {
|
||||||
|
if (!currentWindow?.isMaximized()) {
|
||||||
|
currentWindow?.maximize();
|
||||||
|
} else {
|
||||||
|
currentWindow?.unmaximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupCloseHandlers() {
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (!IS_MAC_OS) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', (event) => {
|
||||||
|
if (IS_MAC_OS && !forceQuit) {
|
||||||
|
event.preventDefault();
|
||||||
|
forceQuit = true;
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
const hasActiveWindow = BrowserWindow.getAllWindows().length > 0;
|
||||||
|
|
||||||
|
if (!hasActiveWindow) {
|
||||||
|
createWindow();
|
||||||
|
} else if (IS_MAC_OS) {
|
||||||
|
forceQuit = false;
|
||||||
|
|
||||||
|
const currentWindow = Array.from(windows).pop();
|
||||||
|
currentWindow?.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { addActionHandler, setGlobal } from '../../index';
|
import { addActionHandler, setGlobal } from '../../index';
|
||||||
|
|
||||||
|
import { IS_ELECTRON } from '../../../config';
|
||||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -80,7 +81,12 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
|||||||
addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => {
|
||||||
const { chatId, threadId = MAIN_THREAD_ID } = payload;
|
const { chatId, threadId = MAIN_THREAD_ID } = payload;
|
||||||
|
|
||||||
window.open(createMessageHashUrl(chatId, 'thread', threadId), '_blank');
|
const hashUrl = createMessageHashUrl(chatId, 'thread', threadId);
|
||||||
|
if (IS_ELECTRON) {
|
||||||
|
window.electron!.openNewWindow(hashUrl);
|
||||||
|
} else {
|
||||||
|
window.open(hashUrl, '_blank');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('openPreviousChat', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openPreviousChat', (global, actions, payload): ActionReturnType => {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { addCallback } from '../../../lib/teact/teactn';
|
import { addCallback } from '../../../lib/teact/teactn';
|
||||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
|
|
||||||
|
import { IS_ELECTRON } from '../../../config';
|
||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
import {
|
import {
|
||||||
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV,
|
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV, IS_WINDOWS, IS_LINUX,
|
||||||
} from '../../../util/windowEnvironment';
|
} from '../../../util/windowEnvironment';
|
||||||
import { setLanguage } from '../../../util/langProvider';
|
import { setLanguage } from '../../../util/langProvider';
|
||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
@ -136,10 +137,17 @@ addCallback((global: GlobalState) => {
|
|||||||
document.body.classList.add('is-android');
|
document.body.classList.add('is-android');
|
||||||
} else if (IS_MAC_OS) {
|
} else if (IS_MAC_OS) {
|
||||||
document.body.classList.add('is-macos');
|
document.body.classList.add('is-macos');
|
||||||
|
} else if (IS_WINDOWS) {
|
||||||
|
document.body.classList.add('is-windows');
|
||||||
|
} else if (IS_LINUX) {
|
||||||
|
document.body.classList.add('is-linux');
|
||||||
}
|
}
|
||||||
if (IS_SAFARI) {
|
if (IS_SAFARI) {
|
||||||
document.body.classList.add('is-safari');
|
document.body.classList.add('is-safari');
|
||||||
}
|
}
|
||||||
|
if (IS_ELECTRON) {
|
||||||
|
document.body.classList.add('is-electron');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
switchTheme(theme, selectCanAnimateInterface(global));
|
switchTheme(theme, selectCanAnimateInterface(global));
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
|||||||
import type { ActionReturnType, GlobalState } from '../../types';
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
|
APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE, IS_ELECTRON,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import getReadableErrorText from '../../../util/getReadableErrorText';
|
import getReadableErrorText from '../../../util/getReadableErrorText';
|
||||||
import {
|
import {
|
||||||
@ -580,6 +580,10 @@ addActionHandler('updateArchiveSettings', (global, actions, payload): ActionRetu
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
||||||
|
if (IS_ELECTRON) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/;
|
const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/;
|
||||||
|
|
||||||
fetch(`${APP_VERSION_URL}?${Date.now()}`)
|
fetch(`${APP_VERSION_URL}?${Date.now()}`)
|
||||||
@ -604,6 +608,15 @@ addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('setIsAppUpdateAvailable', (global, action, payload): ActionReturnType => {
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
isUpdateAvailable: Boolean(payload),
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('afterHangUp', (global): ActionReturnType => {
|
addActionHandler('afterHangUp', (global): ActionReturnType => {
|
||||||
if (!selectTabState(global, getCurrentTabId()).multitabNextAction) return;
|
if (!selectTabState(global, getCurrentTabId()).multitabNextAction) return;
|
||||||
reestablishMasterToSelf();
|
reestablishMasterToSelf();
|
||||||
|
|||||||
@ -1593,6 +1593,7 @@ export interface ActionPayloads {
|
|||||||
openLimitReachedModal: { limit: ApiLimitTypeWithModal } & WithTabId;
|
openLimitReachedModal: { limit: ApiLimitTypeWithModal } & WithTabId;
|
||||||
closeLimitReachedModal: WithTabId | undefined;
|
closeLimitReachedModal: WithTabId | undefined;
|
||||||
checkAppVersion: undefined;
|
checkAppVersion: undefined;
|
||||||
|
setIsAppUpdateAvailable: boolean;
|
||||||
setGlobalSearchClosing: ({
|
setGlobalSearchClosing: ({
|
||||||
isClosing?: boolean;
|
isClosing?: boolean;
|
||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { ApiChat, ApiUser } from '../api/types';
|
|||||||
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
||||||
|
|
||||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/windowEnvironment';
|
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/windowEnvironment';
|
||||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../config';
|
import { IS_ELECTRON, SERVICE_NOTIFICATIONS_USER_ID } from '../config';
|
||||||
import {
|
import {
|
||||||
isChatArchived, getCanDeleteChat, isUserId, isChatChannel, isChatGroup,
|
isChatArchived, getCanDeleteChat, isUserId, isChatChannel, isChatGroup,
|
||||||
} from '../global/helpers';
|
} from '../global/helpers';
|
||||||
@ -54,7 +54,7 @@ const useChatContextActions = ({
|
|||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
||||||
title: 'Open in new tab',
|
title: IS_ELECTRON ? 'Open in new window' : 'Open in new tab',
|
||||||
icon: 'open-in-new-tab',
|
icon: 'open-in-new-tab',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
openChatInNewTab({ chatId: chat.id });
|
openChatInNewTab({ chatId: chat.id });
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const useContextMenuHandlers = (
|
|||||||
) => {
|
) => {
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
||||||
|
const [contextMenuTarget, setContextMenuTarget] = useState<HTMLElement | undefined>(undefined);
|
||||||
|
|
||||||
const handleBeforeContextMenu = useCallback((e: React.MouseEvent) => {
|
const handleBeforeContextMenu = useCallback((e: React.MouseEvent) => {
|
||||||
if (!isMenuDisabled && e.button === 2) {
|
if (!isMenuDisabled && e.button === 2) {
|
||||||
@ -50,6 +51,7 @@ const useContextMenuHandlers = (
|
|||||||
|
|
||||||
setIsContextMenuOpen(true);
|
setIsContextMenuOpen(true);
|
||||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||||
|
setContextMenuTarget(e.target as HTMLElement);
|
||||||
}, [isMenuDisabled, shouldDisableOnLink, contextMenuPosition]);
|
}, [isMenuDisabled, shouldDisableOnLink, contextMenuPosition]);
|
||||||
|
|
||||||
const handleContextMenuClose = useCallback(() => {
|
const handleContextMenuClose = useCallback(() => {
|
||||||
@ -151,6 +153,7 @@ const useContextMenuHandlers = (
|
|||||||
return {
|
return {
|
||||||
isContextMenuOpen,
|
isContextMenuOpen,
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
|
contextMenuTarget,
|
||||||
handleBeforeContextMenu,
|
handleBeforeContextMenu,
|
||||||
handleContextMenu,
|
handleContextMenu,
|
||||||
handleContextMenuClose,
|
handleContextMenuClose,
|
||||||
|
|||||||
69
src/hooks/useElectronDrag.ts
Normal file
69
src/hooks/useElectronDrag.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { useEffect, useRef } from '../lib/teact/teact';
|
||||||
|
import { IS_ELECTRON } from '../config';
|
||||||
|
import { IS_MAC_OS } from '../util/windowEnvironment';
|
||||||
|
|
||||||
|
const DRAG_DISTANCE_THRESHOLD = 5;
|
||||||
|
|
||||||
|
const useElectronDrag = (ref: RefObject<HTMLDivElement>) => {
|
||||||
|
const isDragging = useRef(false);
|
||||||
|
|
||||||
|
const x = useRef(window.screenX);
|
||||||
|
const y = useRef(window.screenY);
|
||||||
|
const distance = useRef(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element: HTMLDivElement | null = ref.current;
|
||||||
|
if (!element || !(IS_ELECTRON && IS_MAC_OS)) return undefined;
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
distance.current = 0;
|
||||||
|
|
||||||
|
if (isDragging.current) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
isDragging.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMousedown = (event: MouseEvent) => {
|
||||||
|
if (isDragging.current) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = (event: MouseEvent) => {
|
||||||
|
if (event.buttons === 1) {
|
||||||
|
distance.current += Math.sqrt((x.current - window.screenX) ** 2 + (y.current - window.screenY) ** 2);
|
||||||
|
|
||||||
|
x.current = window.screenX;
|
||||||
|
y.current = window.screenY;
|
||||||
|
|
||||||
|
if (!isDragging.current && distance.current > DRAG_DISTANCE_THRESHOLD) {
|
||||||
|
isDragging.current = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = (event: MouseEvent) => {
|
||||||
|
if (event.currentTarget === event.target) {
|
||||||
|
window.electron?.handleDoubleClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener('click', handleClick);
|
||||||
|
element.addEventListener('mousedown', handleMousedown);
|
||||||
|
element.addEventListener('mousemove', handleDrag);
|
||||||
|
element.addEventListener('dblclick', handleDoubleClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener('click', handleClick);
|
||||||
|
element.removeEventListener('mouseup', handleMousedown);
|
||||||
|
element.removeEventListener('mousemove', handleDrag);
|
||||||
|
element.removeEventListener('dblclick', handleDoubleClick);
|
||||||
|
};
|
||||||
|
}, [ref]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useElectronDrag;
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import { useLayoutEffect, useState, useEffect } from '../lib/teact/teact';
|
import { useLayoutEffect, useState, useEffect } from '../lib/teact/teact';
|
||||||
import { IS_IOS } from '../util/windowEnvironment';
|
import { IS_IOS } from '../util/windowEnvironment';
|
||||||
|
|
||||||
|
import { ElectronEvent } from '../types/electron';
|
||||||
|
|
||||||
type RefType = {
|
type RefType = {
|
||||||
current: HTMLVideoElement | null;
|
current: HTMLVideoElement | null;
|
||||||
};
|
};
|
||||||
@ -79,11 +81,16 @@ export const useFullscreenStatus = () => {
|
|||||||
setIsFullscreen(checkIfFullscreen());
|
setIsFullscreen(checkIfFullscreen());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeElectronListener = window.electron?.on(ElectronEvent.FULLSCREEN_CHANGE, setIsFullscreen);
|
||||||
|
window.electron?.isFullscreen().then(setIsFullscreen);
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', listener, false);
|
document.addEventListener('fullscreenchange', listener, false);
|
||||||
document.addEventListener('webkitfullscreenchange', listener, false);
|
document.addEventListener('webkitfullscreenchange', listener, false);
|
||||||
document.addEventListener('mozfullscreenchange', listener, false);
|
document.addEventListener('mozfullscreenchange', listener, false);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
removeElectronListener?.();
|
||||||
|
|
||||||
document.removeEventListener('fullscreenchange', listener, false);
|
document.removeEventListener('fullscreenchange', listener, false);
|
||||||
document.removeEventListener('webkitfullscreenchange', listener, false);
|
document.removeEventListener('webkitfullscreenchange', listener, false);
|
||||||
document.removeEventListener('mozfullscreenchange', listener, false);
|
document.removeEventListener('mozfullscreenchange', listener, false);
|
||||||
|
|||||||
26
src/types/electron.ts
Normal file
26
src/types/electron.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export enum ElectronEvent {
|
||||||
|
FULLSCREEN_CHANGE = 'fullscreen-change',
|
||||||
|
UPDATE_ERROR = 'update-error',
|
||||||
|
UPDATE_DOWNLOADED = 'update-downloaded',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ElectronAction {
|
||||||
|
GET_IS_FULLSCREEN = 'get-is-fullscreen',
|
||||||
|
INSTALL_UPDATE = 'install-update',
|
||||||
|
HANDLE_DOUBLE_CLICK = 'handle-double-click',
|
||||||
|
OPEN_NEW_WINDOW = 'open-new-window',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElectronApi {
|
||||||
|
isFullscreen: () => Promise<boolean>;
|
||||||
|
installUpdate: () => Promise<void>;
|
||||||
|
handleDoubleClick: () => Promise<void>;
|
||||||
|
openNewWindow: (url: string) => Promise<void>;
|
||||||
|
on: (eventName: ElectronEvent, callback: any) => VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron?: ElectronApi;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { IS_ELECTRON, ELECTRON_HOST_URL } from '../config';
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const cacheApi = self.caches;
|
const cacheApi = self.caches;
|
||||||
|
|
||||||
@ -24,7 +26,9 @@ export async function fetch(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
const request = new Request(key.replace(/:/g, '_'));
|
const request = IS_ELECTRON
|
||||||
|
? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}`
|
||||||
|
: new Request(key.replace(/:/g, '_'));
|
||||||
const cache = await cacheApi.open(cacheName);
|
const cache = await cacheApi.open(cacheName);
|
||||||
const response = await cache.match(request);
|
const response = await cache.match(request);
|
||||||
if (!response) {
|
if (!response) {
|
||||||
@ -82,7 +86,9 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl
|
|||||||
? data
|
? data
|
||||||
: JSON.stringify(data);
|
: JSON.stringify(data);
|
||||||
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
// To avoid the error "Request scheme 'webdocument' is unsupported"
|
||||||
const request = new Request(key.replace(/:/g, '_'));
|
const request = IS_ELECTRON
|
||||||
|
? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}`
|
||||||
|
: new Request(key.replace(/:/g, '_'));
|
||||||
const response = new Response(cacheData);
|
const response = new Response(cacheData);
|
||||||
const cache = await cacheApi.open(cacheName);
|
const cache = await cacheApi.open(cacheName);
|
||||||
await cache.put(request, response);
|
await cache.put(request, response);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEBUG, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS,
|
DEBUG, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS, IS_ELECTRON, ELECTRON_HOST_URL,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { callApi, cancelApiProgress } from '../api/gramjs';
|
import { callApi, cancelApiProgress } from '../api/gramjs';
|
||||||
import * as cacheApi from './cacheApi';
|
import * as cacheApi from './cacheApi';
|
||||||
@ -27,7 +27,7 @@ const asCacheApiType = {
|
|||||||
[ApiMediaFormat.Stream]: undefined,
|
[ApiMediaFormat.Stream]: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROGRESSIVE_URL_PREFIX = './progressive/';
|
const PROGRESSIVE_URL_PREFIX = `${IS_ELECTRON ? ELECTRON_HOST_URL : '.'}/progressive/`;
|
||||||
const URL_DOWNLOAD_PREFIX = './download/';
|
const URL_DOWNLOAD_PREFIX = './download/';
|
||||||
|
|
||||||
const memoryCache = new Map<string, ApiPreparedMedia>();
|
const memoryCache = new Map<string, ApiPreparedMedia>();
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import type {
|
|||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import { ApiMediaFormat } from '../api/types';
|
import { ApiMediaFormat } from '../api/types';
|
||||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||||
import { APP_NAME, DEBUG, IS_TEST } from '../config';
|
import {
|
||||||
|
APP_NAME, DEBUG, IS_ELECTRON, IS_TEST,
|
||||||
|
} from '../config';
|
||||||
import { getActions, getGlobal, setGlobal } from '../global';
|
import { getActions, getGlobal, setGlobal } from '../global';
|
||||||
import {
|
import {
|
||||||
getChatAvatarHash,
|
getChatAvatarHash,
|
||||||
@ -44,7 +46,8 @@ function getDeviceToken(subscription: PushSubscription) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkIfPushSupported() {
|
function checkIfPushSupported() {
|
||||||
if (!IS_SERVICE_WORKER_SUPPORTED) return false;
|
if (!IS_SERVICE_WORKER_SUPPORTED || IS_ELECTRON) return false;
|
||||||
|
|
||||||
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -70,6 +73,7 @@ function checkIfPushSupported() {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -446,7 +450,7 @@ export async function notifyAboutMessage({
|
|||||||
if (!checkIfShouldNotify(chat, message)) return;
|
if (!checkIfShouldNotify(chat, message)) return;
|
||||||
const areNotificationsSupported = checkIfNotificationsSupported();
|
const areNotificationsSupported = checkIfNotificationsSupported();
|
||||||
if (!hasWebNotifications || !areNotificationsSupported) {
|
if (!hasWebNotifications || !areNotificationsSupported) {
|
||||||
if (!message.isSilent && !isReaction) {
|
if (!message.isSilent && !isReaction && !IS_ELECTRON) {
|
||||||
// Only play sound if web notifications are disabled
|
// Only play sound if web notifications are disabled
|
||||||
playNotifySoundDebounced(String(message.id) || chat.id);
|
playNotifySoundDebounced(String(message.id) || chat.id);
|
||||||
}
|
}
|
||||||
@ -520,7 +524,7 @@ export async function notifyAboutMessage({
|
|||||||
// Play sound when notification is displayed
|
// Play sound when notification is displayed
|
||||||
notification.onshow = () => {
|
notification.onshow = () => {
|
||||||
// TODO Update when reaction badges are implemented
|
// TODO Update when reaction badges are implemented
|
||||||
if (isReaction || message.isSilent) return;
|
if (isReaction || message.isSilent || IS_ELECTRON) return;
|
||||||
playNotifySoundDebounced(String(message.id) || chat.id);
|
playNotifySoundDebounced(String(message.id) || chat.id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { APP_VERSION, DEBUG, IS_MOCKED_CLIENT } from '../config';
|
import {
|
||||||
|
APP_VERSION, DEBUG, IS_MOCKED_CLIENT, IS_ELECTRON,
|
||||||
|
} from '../config';
|
||||||
import { getGlobal } from '../global';
|
import { getGlobal } from '../global';
|
||||||
import { hasStoredSession } from './sessions';
|
import { hasStoredSession } from './sessions';
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ const saveSync = (authed: boolean) => {
|
|||||||
let lastTimeout: NodeJS.Timeout | undefined;
|
let lastTimeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
export const forceWebsync = (authed: boolean) => {
|
export const forceWebsync = (authed: boolean) => {
|
||||||
if (IS_MOCKED_CLIENT) return undefined;
|
if (IS_MOCKED_CLIENT || IS_ELECTRON) return undefined;
|
||||||
const currentTs = getTs();
|
const currentTs = getTs();
|
||||||
|
|
||||||
const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
|
const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
|
||||||
@ -67,13 +69,13 @@ export const forceWebsync = (authed: boolean) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function stopWebsync() {
|
export function stopWebsync() {
|
||||||
if (DEBUG) return;
|
if (DEBUG || IS_ELECTRON) return;
|
||||||
|
|
||||||
if (lastTimeout) clearTimeout(lastTimeout);
|
if (lastTimeout) clearTimeout(lastTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startWebsync() {
|
export function startWebsync() {
|
||||||
if (DEBUG) {
|
if (DEBUG || IS_ELECTRON) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
IS_TEST,
|
IS_TEST,
|
||||||
|
IS_ELECTRON,
|
||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
VIDEO_MOV_TYPE,
|
VIDEO_MOV_TYPE,
|
||||||
CONTENT_TYPES_WITH_PREVIEW,
|
CONTENT_TYPES_WITH_PREVIEW,
|
||||||
@ -37,6 +38,8 @@ export function getPlatform() {
|
|||||||
export const IS_PRODUCTION_HOST = window.location.host === PRODUCTION_HOSTNAME;
|
export const IS_PRODUCTION_HOST = window.location.host === PRODUCTION_HOSTNAME;
|
||||||
export const PLATFORM_ENV = getPlatform();
|
export const PLATFORM_ENV = getPlatform();
|
||||||
export const IS_MAC_OS = PLATFORM_ENV === 'macOS';
|
export const IS_MAC_OS = PLATFORM_ENV === 'macOS';
|
||||||
|
export const IS_WINDOWS = PLATFORM_ENV === 'Windows';
|
||||||
|
export const IS_LINUX = PLATFORM_ENV === 'Linux';
|
||||||
export const IS_IOS = PLATFORM_ENV === 'iOS';
|
export const IS_IOS = PLATFORM_ENV === 'iOS';
|
||||||
export const IS_ANDROID = PLATFORM_ENV === 'Android';
|
export const IS_ANDROID = PLATFORM_ENV === 'Android';
|
||||||
export const IS_MOBILE = IS_IOS || IS_ANDROID;
|
export const IS_MOBILE = IS_IOS || IS_ANDROID;
|
||||||
@ -57,6 +60,8 @@ export const IS_PWA = (
|
|||||||
|| document.referrer.includes('android-app://')
|
|| document.referrer.includes('android-app://')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IS_APP = IS_PWA || IS_ELECTRON;
|
||||||
|
|
||||||
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches;
|
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches;
|
||||||
export const IS_VOICE_RECORDING_SUPPORTED = Boolean(
|
export const IS_VOICE_RECORDING_SUPPORTED = Boolean(
|
||||||
window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && (
|
window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && (
|
||||||
|
|||||||
@ -26,7 +26,8 @@
|
|||||||
"tests",
|
"tests",
|
||||||
"plugins",
|
"plugins",
|
||||||
"dev",
|
"dev",
|
||||||
"webpack.config.js",
|
"webpack.config.ts",
|
||||||
|
"webpack-electron.config.ts",
|
||||||
"babel.config.js",
|
"babel.config.js",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
40
webpack-electron.config.ts
Normal file
40
webpack-electron.config.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { EnvironmentPlugin } from 'webpack';
|
||||||
|
|
||||||
|
const { APP_ENV = 'production' } = process.env;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
target: 'node',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
electron: './src/electron/main.ts',
|
||||||
|
preload: './src/electron/preload.ts',
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new EnvironmentPlugin({ APP_ENV }),
|
||||||
|
],
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.(ts|tsx|js)$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
|
externals: {
|
||||||
|
electron: 'require("electron")',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,25 +1,29 @@
|
|||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const dotenv = require('dotenv');
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
const {
|
import {
|
||||||
DefinePlugin,
|
DefinePlugin,
|
||||||
EnvironmentPlugin,
|
EnvironmentPlugin,
|
||||||
ProvidePlugin,
|
ProvidePlugin,
|
||||||
ContextReplacementPlugin,
|
ContextReplacementPlugin,
|
||||||
NormalModuleReplacementPlugin,
|
NormalModuleReplacementPlugin,
|
||||||
} = require('webpack');
|
} from 'webpack';
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
import { GitRevisionPlugin } from 'git-revision-webpack-plugin';
|
||||||
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;
|
import StatoscopeWebpackPlugin from '@statoscope/webpack-plugin';
|
||||||
const WebpackContextExtension = require('./dev/webpackContextExtension');
|
|
||||||
const appVersion = require('./package.json').version;
|
import type { Configuration, Compiler } from 'webpack';
|
||||||
|
import 'webpack-dev-server';
|
||||||
|
|
||||||
|
import { version as appVersion } from './package.json';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
HEAD,
|
HEAD,
|
||||||
APP_ENV = 'production',
|
APP_ENV = 'production',
|
||||||
APP_MOCKED_CLIENT = '',
|
APP_MOCKED_CLIENT = '',
|
||||||
|
IS_ELECTRON,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@ -33,7 +37,10 @@ const {
|
|||||||
APP_TITLE = DEFAULT_APP_TITLE,
|
APP_TITLE = DEFAULT_APP_TITLE,
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
module.exports = (_env, { mode = 'production' }) => {
|
export default function createConfig(
|
||||||
|
_: any,
|
||||||
|
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
||||||
|
): Configuration {
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
entry: './src/index.tsx',
|
entry: './src/index.tsx',
|
||||||
@ -146,13 +153,13 @@ module.exports = (_env, { mode = 'production' }) => {
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
...(APP_ENV === 'staging' ? [{
|
...(APP_ENV === 'staging' ? [{
|
||||||
apply: (compiler) => {
|
apply: (compiler: Compiler) => {
|
||||||
compiler.hooks.compile.tap('Before Compilation', async () => {
|
compiler.hooks.compile.tap('Before Compilation', async () => {
|
||||||
try {
|
try {
|
||||||
const stats = await fetch(STATOSCOPE_REFERENCE_URL).then((res) => res.text());
|
const stats = await fetch(STATOSCOPE_REFERENCE_URL).then((res) => res.text());
|
||||||
fs.writeFileSync(path.resolve('./public/reference.json'), stats);
|
fs.writeFileSync(path.resolve('./public/reference.json'), stats);
|
||||||
isReferenceFetched = true;
|
isReferenceFetched = true;
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Failed to fetch reference statoscope stats: ', err.message);
|
console.warn('Failed to fetch reference statoscope stats: ', err.message);
|
||||||
}
|
}
|
||||||
@ -187,6 +194,7 @@ module.exports = (_env, { mode = 'production' }) => {
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
APP_NAME: null,
|
APP_NAME: null,
|
||||||
APP_VERSION: appVersion,
|
APP_VERSION: appVersion,
|
||||||
|
IS_ELECTRON: false,
|
||||||
APP_TITLE,
|
APP_TITLE,
|
||||||
RELEASE_DATETIME: Date.now(),
|
RELEASE_DATETIME: Date.now(),
|
||||||
TELEGRAM_T_API_ID: undefined,
|
TELEGRAM_T_API_ID: undefined,
|
||||||
@ -212,14 +220,14 @@ module.exports = (_env, { mode = 'production' }) => {
|
|||||||
saveStatsTo: path.resolve('./public/build-stats.json'),
|
saveStatsTo: path.resolve('./public/build-stats.json'),
|
||||||
normalizeStats: true,
|
normalizeStats: true,
|
||||||
open: 'file',
|
open: 'file',
|
||||||
extensions: [new WebpackContextExtension()],
|
extensions: [new WebpackContextExtension()], // eslint-disable-line @typescript-eslint/no-use-before-define
|
||||||
...(APP_ENV === 'staging' && isReferenceFetched && {
|
...(APP_ENV === 'staging' && isReferenceFetched && {
|
||||||
additionalStats: ['./public/reference.json'],
|
additionalStats: ['./public/reference.json'],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
devtool: 'source-map',
|
devtool: APP_ENV === 'production' && IS_ELECTRON ? undefined : 'source-map',
|
||||||
|
|
||||||
...(APP_ENV !== 'production' && {
|
...(APP_ENV !== 'production' && {
|
||||||
optimization: {
|
optimization: {
|
||||||
@ -227,11 +235,30 @@ module.exports = (_env, { mode = 'production' }) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
function getGitMetadata() {
|
function getGitMetadata() {
|
||||||
const gitRevisionPlugin = new GitRevisionPlugin();
|
const gitRevisionPlugin = new GitRevisionPlugin();
|
||||||
const branch = HEAD || gitRevisionPlugin.branch();
|
const branch = HEAD || gitRevisionPlugin.branch();
|
||||||
const commit = gitRevisionPlugin.commithash().substring(0, 7);
|
const commit = gitRevisionPlugin.commithash()?.substring(0, 7);
|
||||||
return { branch, commit };
|
return { branch, commit };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WebpackContextExtension {
|
||||||
|
context: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.context = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCompiler(compiler: Compiler) {
|
||||||
|
this.context = compiler.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtension() {
|
||||||
|
return {
|
||||||
|
descriptor: { name: 'custom-webpack-extension-context', version: '1.0.0' },
|
||||||
|
payload: { context: this.context },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user