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())
|
||||
```
|
||||
|
||||
## 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
|
||||
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",
|
||||
"prepare": "husky install",
|
||||
"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)"
|
||||
},
|
||||
"engines": {
|
||||
@ -40,6 +46,9 @@
|
||||
"*.{ts,tsx,js}": "eslint --fix",
|
||||
"*.{css,scss}": "stylelint --fix"
|
||||
},
|
||||
"electronmon": {
|
||||
"logLevel": "quiet"
|
||||
},
|
||||
"author": "Alexander Zinchuk (alexander@zinchuk.com)",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
@ -51,6 +60,8 @@
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@babel/register": "^7.21.0",
|
||||
"@electron/rebuild": "^3.2.10",
|
||||
"@glen/jest-raw-loader": "^2.0.0",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@statoscope/cli": "^5.26.1",
|
||||
@ -61,6 +72,8 @@
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/react": "^18.0.28",
|
||||
"@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/parser": "^5.55.0",
|
||||
"@webpack-cli/serve": "^2.0.1",
|
||||
@ -69,9 +82,15 @@
|
||||
"babel-plugin-transform-import-meta": "^2.2.0",
|
||||
"browserlist": "^1.0.1",
|
||||
"buffer": "^6.0.3",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.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-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
@ -129,7 +148,8 @@
|
||||
"os-browserify": "^0.3.0",
|
||||
"pako": "^2.1.0",
|
||||
"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": {
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
body.is-electron #auth-phone-number-form & {
|
||||
padding-top: 3rem;
|
||||
|
||||
.form {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#auth-registration-form,
|
||||
@ -68,10 +76,18 @@
|
||||
#auth-qr-form {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
body.is-electron.is-macos & {
|
||||
-webkit-app-region: drag;
|
||||
|
||||
.input-group {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#auth-phone-number-form {
|
||||
form {
|
||||
.form {
|
||||
min-height: 26.25rem;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 type { GlobalState } from '../../global/types';
|
||||
@ -8,6 +8,7 @@ import '../../global/actions/initial';
|
||||
import { PLATFORM_ENV } from '../../util/windowEnvironment';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||
|
||||
import Transition from '../ui/Transition';
|
||||
import AuthPhoneNumber from './AuthPhoneNumber';
|
||||
@ -43,6 +44,10 @@ const Auth: FC<StateProps> = ({
|
||||
onBack: handleChangeAuthorizationMethod,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useElectronDrag(containerRef);
|
||||
|
||||
// For animation purposes
|
||||
const renderingAuthState = useCurrentOrPrev(
|
||||
authState !== 'authorizationStateReady' ? authState : undefined,
|
||||
@ -84,7 +89,7 @@ const Auth: FC<StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition activeKey={getActiveKey()} name="fade" className="Auth">
|
||||
<Transition activeKey={getActiveKey()} name="fade" className="Auth" ref={containerRef}>
|
||||
{getScreen()}
|
||||
</Transition>
|
||||
);
|
||||
|
||||
@ -216,7 +216,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
||||
<div id="logo" />
|
||||
<h1>Telegram</h1>
|
||||
<p className="note">{lang('StartText')}</p>
|
||||
<form action="" onSubmit={handleSubmit}>
|
||||
<form className="form" action="" onSubmit={handleSubmit}>
|
||||
<CountryCodeInput
|
||||
id="sign-in-phone-code"
|
||||
value={country}
|
||||
|
||||
@ -4,7 +4,9 @@ import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import type { TextPart } from '../../../types';
|
||||
|
||||
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 {
|
||||
fixNonStandardEmoji,
|
||||
@ -108,7 +110,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
||||
return emojis.reduce((emojiResult: TextPart[], emoji, i) => {
|
||||
const code = nativeToUnifiedExtendedWithCache(emoji);
|
||||
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(
|
||||
'emoji',
|
||||
size === 'small' && 'emoji-small',
|
||||
@ -132,7 +134,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
|
||||
`<img\
|
||||
draggable="false"\
|
||||
class="${className}"\
|
||||
src="./img-apple-${size === 'big' ? '160' : '64'}/${code}.png"\
|
||||
src={src}\
|
||||
alt="${emoji}"\
|
||||
/>`,
|
||||
);
|
||||
|
||||
@ -40,4 +40,27 @@
|
||||
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 { 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 { selectCurrentChat, selectIsForumPanelOpen, selectTabState } from '../../global/selectors';
|
||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||
@ -381,11 +381,11 @@ function LeftColumn({
|
||||
useHotkeys({
|
||||
'Mod+Shift+F': handleHotkeySearch,
|
||||
'Mod+Shift+S': handleHotkeySavedMessages,
|
||||
...(IS_PWA && {
|
||||
...(IS_APP && {
|
||||
'Mod+0': handleHotkeySavedMessages,
|
||||
'Mod+9': handleArchivedChats,
|
||||
}),
|
||||
...(IS_MAC_OS && IS_PWA && { 'Mod+,': handleHotkeySettings }),
|
||||
...(IS_MAC_OS && IS_APP && { 'Mod+,': handleHotkeySettings }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
CHAT_HEIGHT_PX,
|
||||
CHAT_LIST_SLICE,
|
||||
} 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 buildClassName from '../../../util/buildClassName';
|
||||
|
||||
@ -96,7 +96,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
|
||||
// Support <Cmd>+<Digit> to navigate between chats
|
||||
useEffect(() => {
|
||||
if (!isActive || !orderedIds || !IS_PWA) {
|
||||
if (!isActive || !orderedIds || !IS_APP) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -217,7 +217,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
onTransitionEnd={!isOpen ? onCloseAnimationEnd : undefined}
|
||||
>
|
||||
<div className="left-header">
|
||||
<div id="TopicListHeader" className="left-header">
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
|
||||
@ -8,6 +8,7 @@ import type { SettingsScreens } from '../../../types';
|
||||
import { LeftColumnContent } from '../../../types';
|
||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
import { IS_ELECTRON } from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
@ -117,7 +118,11 @@ const LeftMain: FC<OwnProps> = ({
|
||||
}, [closeForumPanel, onContentChange]);
|
||||
|
||||
const handleUpdateClick = useCallback(() => {
|
||||
window.location.reload();
|
||||
if (IS_ELECTRON) {
|
||||
window.electron?.installUpdate();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectNewChannel = useCallback(() => {
|
||||
|
||||
@ -102,6 +102,10 @@
|
||||
.extra-spacing {
|
||||
position: relative;
|
||||
margin-left: 0.8125rem;
|
||||
|
||||
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-status-effect {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo,
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -18,14 +18,15 @@ import {
|
||||
FEEDBACK_URL,
|
||||
IS_BETA,
|
||||
IS_TEST,
|
||||
IS_ELECTRON,
|
||||
PRODUCTION_HOSTNAME,
|
||||
} from '../../../config';
|
||||
import { IS_APP } from '../../../util/windowEnvironment';
|
||||
import {
|
||||
INITIAL_PERFORMANCE_STATE_MAX,
|
||||
INITIAL_PERFORMANCE_STATE_MID,
|
||||
INITIAL_PERFORMANCE_STATE_MIN,
|
||||
} from '../../../global/initialState';
|
||||
import { IS_PWA } from '../../../util/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateToString } from '../../../util/dateFormat';
|
||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||
@ -40,6 +41,8 @@ import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||
import { getPromptInstall } from '../../../util/installPrompt';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
||||
import { useFullscreenStatus } from '../../../hooks/useFullscreen';
|
||||
import useElectronDrag from '../../../hooks/useElectronDrag';
|
||||
import { useFolderManagerForUnreadCounters } from '../../../hooks/useFolderManager';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
|
||||
@ -163,7 +166,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
'Ctrl+Shift+L': handleLockScreenHotkey,
|
||||
'Alt+Shift+L': handleLockScreenHotkey,
|
||||
'Meta+Shift+L': handleLockScreenHotkey,
|
||||
...(IS_PWA && { 'Mod+L': handleLockScreenHotkey }),
|
||||
...(IS_APP && { 'Mod+L': handleLockScreenHotkey }),
|
||||
} : undefined);
|
||||
|
||||
const withOtherVersions = window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST;
|
||||
@ -266,12 +269,18 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
? (animationLevel === ANIMATION_LEVEL_MAX ? 'max' : 'mid')
|
||||
: 'min';
|
||||
|
||||
const isFullscreen = useFullscreenStatus();
|
||||
|
||||
// Disable dropdown menu RTL animation for resize
|
||||
const {
|
||||
shouldDisableDropdownMenuTransitionRef,
|
||||
handleDropdownMenuTransitionEnd,
|
||||
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
useElectronDrag(headerRef);
|
||||
|
||||
const menuItems = useMemo(() => (
|
||||
<>
|
||||
<MenuItem
|
||||
@ -369,17 +378,19 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return (
|
||||
<div className="LeftMainHeader">
|
||||
<div id="LeftMainHeader" className="left-header">
|
||||
<div id="LeftMainHeader" className="left-header" ref={headerRef}>
|
||||
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
||||
<DropdownMenu
|
||||
trigger={MainButton}
|
||||
footer={`${APP_NAME} ${versionString}`}
|
||||
className={buildClassName(
|
||||
'main-menu',
|
||||
lang.isRtl && 'rtl',
|
||||
shouldHideSearch && lang.isRtl && 'right-aligned',
|
||||
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
|
||||
)}
|
||||
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
|
||||
transformOriginX={IS_ELECTRON && !isFullscreen ? 90 : undefined}
|
||||
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
|
||||
>
|
||||
{menuItems}
|
||||
|
||||
@ -15,10 +15,11 @@ import type {
|
||||
ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||
import { ElectronEvent } from '../../types/electron';
|
||||
|
||||
import '../../global/actions/all';
|
||||
import {
|
||||
BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER,
|
||||
BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER, IS_ELECTRON,
|
||||
} from '../../config';
|
||||
import { IS_ANDROID } from '../../util/windowEnvironment';
|
||||
import {
|
||||
@ -52,6 +53,7 @@ import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useInterval from '../../hooks/useInterval';
|
||||
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useTimeout from '../../hooks/useTimeout';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -239,6 +241,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
loadTopReactions,
|
||||
loadRecentReactions,
|
||||
loadFeaturedEmojiStickers,
|
||||
setIsAppUpdateAvailable,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -271,7 +274,27 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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
|
||||
useEffect(() => {
|
||||
@ -427,6 +450,8 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
const willAnimateRightColumnRef = useRef(false);
|
||||
const [isNarrowMessageList, setIsNarrowMessageList] = useState(isRightColumnOpen);
|
||||
|
||||
const isFullscreen = useFullscreenStatus();
|
||||
|
||||
// Handle opening right column
|
||||
useSyncEffect(([prevIsRightColumnOpen]) => {
|
||||
if (prevIsRightColumnOpen === undefined || isRightColumnOpen === prevIsRightColumnOpen) {
|
||||
@ -459,6 +484,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
willAnimateRightColumnRef.current && 'right-column-animating',
|
||||
isNarrowMessageList && 'narrow-message-list',
|
||||
shouldSkipHistoryAnimations && 'history-animation-disabled',
|
||||
isFullscreen && 'is-fullscreen',
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
|
||||
@ -97,6 +97,14 @@
|
||||
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,
|
||||
|
||||
@ -41,6 +41,7 @@ import { exitPictureInPictureIfNeeded, usePictureInPictureSignal } from '../../h
|
||||
import useLang from '../../hooks/useLang';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useMediaProps } from './hooks/useMediaProps';
|
||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
|
||||
@ -182,6 +183,10 @@ const MediaViewer: FC<StateProps> = ({
|
||||
}
|
||||
}, [isMobile, isOpen]);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
useElectronDrag(headerRef);
|
||||
|
||||
const forceUpdate = useForceUpdate();
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
|
||||
@ -325,7 +330,7 @@ const MediaViewer: FC<StateProps> = ({
|
||||
shouldAnimateFirstRender
|
||||
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 && (
|
||||
<Button
|
||||
className="media-viewer-close"
|
||||
|
||||
@ -13,7 +13,7 @@ import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import type { IAnchorPosition } 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 {
|
||||
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
|
||||
} from '../../global/helpers';
|
||||
@ -174,7 +174,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
const handleHotkeySearchClick = useCallback((e: KeyboardEvent) => {
|
||||
if (!canSearch || !IS_PWA || e.shiftKey) {
|
||||
if (!canSearch || !IS_APP || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -374,6 +374,20 @@
|
||||
@media (max-width: 600px) {
|
||||
@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 {
|
||||
|
||||
@ -52,6 +52,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
|
||||
@ -409,6 +410,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|
||||
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
|
||||
|
||||
useElectronDrag(componentRef);
|
||||
|
||||
return (
|
||||
<div className="MiddleHeader" ref={componentRef}>
|
||||
<Transition
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { memo, useCallback } 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 { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -29,7 +30,7 @@ const EmojiButton: FC<OwnProps> = ({
|
||||
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);
|
||||
|
||||
return (
|
||||
|
||||
@ -60,6 +60,7 @@ export type OwnProps = {
|
||||
message: ApiMessage;
|
||||
album?: IAlbum;
|
||||
anchor: IAnchorPosition;
|
||||
targetHref?: string;
|
||||
messageListType: MessageListType;
|
||||
noReplies?: boolean;
|
||||
detectedLanguage?: string;
|
||||
@ -121,6 +122,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
customEmojiSets,
|
||||
album,
|
||||
anchor,
|
||||
targetHref,
|
||||
noOptions,
|
||||
canSendNow,
|
||||
hasFullInfo,
|
||||
@ -480,6 +482,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
enabledReactions={enabledReactions}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
anchor={anchor}
|
||||
targetHref={targetHref}
|
||||
canShowReactionsCount={canShowReactionsCount}
|
||||
canShowReactionList={canShowReactionList}
|
||||
canSendNow={canSendNow}
|
||||
|
||||
@ -32,7 +32,7 @@ import { AudioOrigin } from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
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 {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
@ -394,11 +394,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuTarget,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu: onContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, true, IS_ANDROID);
|
||||
} = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, !IS_ELECTRON, IS_ANDROID);
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
@ -1324,6 +1325,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuPosition}
|
||||
targetHref={contextMenuTarget?.matches('a[href]') ? (contextMenuTarget as HTMLAnchorElement).href : undefined}
|
||||
message={message}
|
||||
album={album}
|
||||
chatUsername={chatUsername?.username}
|
||||
|
||||
@ -42,6 +42,7 @@ type OwnProps = {
|
||||
topReactions?: ApiReaction[];
|
||||
isOpen: boolean;
|
||||
anchor: IAnchorPosition;
|
||||
targetHref?: string;
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
canSendNow?: boolean;
|
||||
enabledReactions?: ApiChatReactions;
|
||||
@ -129,6 +130,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
enabledReactions,
|
||||
maxUniqueReactions,
|
||||
anchor,
|
||||
targetHref,
|
||||
canSendNow,
|
||||
canReschedule,
|
||||
canBuyPremium,
|
||||
@ -238,7 +240,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
const copyOptions = isSponsoredMessage
|
||||
? []
|
||||
: getMessageCopyOptions(
|
||||
message, handleAfterCopy, canCopyLink ? onCopyLink : undefined, onCopyMessages, onCopyNumber,
|
||||
message, targetHref, handleAfterCopy, canCopyLink ? onCopyLink : undefined, onCopyMessages, onCopyNumber,
|
||||
);
|
||||
|
||||
const getTriggerElement = useCallback(() => {
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
CLIPBOARD_ITEM_SUPPORTED,
|
||||
copyHtmlToClipboard,
|
||||
copyImageToClipboard,
|
||||
copyTextToClipboard,
|
||||
} from '../../../../util/clipboard';
|
||||
import getMessageIdsForSelectedText from '../../../../util/getMessageIdsForSelectedText';
|
||||
import { renderMessageText } from '../../../common/helpers/renderMessageText';
|
||||
@ -28,6 +29,7 @@ type ICopyOptions = {
|
||||
|
||||
export function getMessageCopyOptions(
|
||||
message: ApiMessage,
|
||||
href?: string,
|
||||
afterEffect?: () => void,
|
||||
onCopyLink?: () => 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
|
||||
const hasSelection = Boolean((
|
||||
selection?.anchorNode?.parentNode
|
||||
|
||||
@ -46,4 +46,10 @@
|
||||
.DropdownMenu {
|
||||
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 React, {
|
||||
memo, useCallback, useEffect, useState,
|
||||
memo, useCallback, useEffect, useState, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
@ -28,6 +28,7 @@ import { getDayStartAt } from '../../util/dateFormat';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
|
||||
import SearchInput from '../ui/SearchInput';
|
||||
@ -515,8 +516,12 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
(shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
useElectronDrag(headerRef);
|
||||
|
||||
return (
|
||||
<div className="RightHeader">
|
||||
<div className="RightHeader" ref={headerRef}>
|
||||
<Button
|
||||
className="close-button"
|
||||
round
|
||||
|
||||
@ -12,6 +12,8 @@ import './DropdownMenu.scss';
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
trigger?: FC<{ onTrigger: () => void; isOpen?: boolean }>;
|
||||
transformOriginX?: number;
|
||||
transformOriginY?: number;
|
||||
positionX?: 'left' | 'right';
|
||||
positionY?: 'top' | 'bottom';
|
||||
footer?: string;
|
||||
@ -28,6 +30,8 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
trigger,
|
||||
className,
|
||||
children,
|
||||
transformOriginX,
|
||||
transformOriginY,
|
||||
positionX = 'left',
|
||||
positionY = 'top',
|
||||
footer,
|
||||
@ -104,6 +108,8 @@ const DropdownMenu: FC<OwnProps> = ({
|
||||
containerRef={dropdownRef}
|
||||
isOpen={isOpen || Boolean(forceOpen)}
|
||||
className={className || ''}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
footer={footer}
|
||||
|
||||
@ -5,17 +5,20 @@ export const APP_VERSION = process.env.APP_VERSION!;
|
||||
export const RELEASE_DATETIME = process.env.RELEASE_DATETIME;
|
||||
|
||||
export const PRODUCTION_HOSTNAME = 'web.telegram.org';
|
||||
|
||||
export const DEBUG = process.env.APP_ENV !== 'production';
|
||||
export const DEBUG_MORE = false;
|
||||
export const STRICTERDOM_ENABLED = DEBUG;
|
||||
export const PRODUCTION_URL = 'https://web.telegram.org/a';
|
||||
|
||||
export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1';
|
||||
export const IS_TEST = process.env.APP_ENV === 'test';
|
||||
export const IS_PERF = process.env.APP_ENV === 'perf';
|
||||
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 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_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 { IS_ELECTRON } from '../../../config';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import {
|
||||
@ -80,7 +81,12 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => {
|
||||
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 => {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_ELECTRON } from '../../../config';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
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';
|
||||
import { setLanguage } from '../../../util/langProvider';
|
||||
import switchTheme from '../../../util/switchTheme';
|
||||
@ -136,10 +137,17 @@ addCallback((global: GlobalState) => {
|
||||
document.body.classList.add('is-android');
|
||||
} else if (IS_MAC_OS) {
|
||||
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) {
|
||||
document.body.classList.add('is-safari');
|
||||
}
|
||||
if (IS_ELECTRON) {
|
||||
document.body.classList.add('is-electron');
|
||||
}
|
||||
});
|
||||
|
||||
switchTheme(theme, selectCanAnimateInterface(global));
|
||||
|
||||
@ -7,7 +7,7 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
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';
|
||||
import getReadableErrorText from '../../../util/getReadableErrorText';
|
||||
import {
|
||||
@ -580,6 +580,10 @@ addActionHandler('updateArchiveSettings', (global, actions, payload): ActionRetu
|
||||
});
|
||||
|
||||
addActionHandler('checkAppVersion', (global): ActionReturnType => {
|
||||
if (IS_ELECTRON) {
|
||||
return;
|
||||
}
|
||||
|
||||
const APP_VERSION_REGEX = /^\d+\.\d+(\.\d+)?$/;
|
||||
|
||||
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 => {
|
||||
if (!selectTabState(global, getCurrentTabId()).multitabNextAction) return;
|
||||
reestablishMasterToSelf();
|
||||
|
||||
@ -1593,6 +1593,7 @@ export interface ActionPayloads {
|
||||
openLimitReachedModal: { limit: ApiLimitTypeWithModal } & WithTabId;
|
||||
closeLimitReachedModal: WithTabId | undefined;
|
||||
checkAppVersion: undefined;
|
||||
setIsAppUpdateAvailable: boolean;
|
||||
setGlobalSearchClosing: ({
|
||||
isClosing?: boolean;
|
||||
} & WithTabId) | undefined;
|
||||
|
||||
@ -5,7 +5,7 @@ import type { ApiChat, ApiUser } from '../api/types';
|
||||
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
||||
|
||||
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 {
|
||||
isChatArchived, getCanDeleteChat, isUserId, isChatChannel, isChatGroup,
|
||||
} from '../global/helpers';
|
||||
@ -54,7 +54,7 @@ const useChatContextActions = ({
|
||||
} = getActions();
|
||||
|
||||
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',
|
||||
handler: () => {
|
||||
openChatInNewTab({ chatId: chat.id });
|
||||
|
||||
@ -25,6 +25,7 @@ const useContextMenuHandlers = (
|
||||
) => {
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
||||
const [contextMenuTarget, setContextMenuTarget] = useState<HTMLElement | undefined>(undefined);
|
||||
|
||||
const handleBeforeContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
if (!isMenuDisabled && e.button === 2) {
|
||||
@ -50,6 +51,7 @@ const useContextMenuHandlers = (
|
||||
|
||||
setIsContextMenuOpen(true);
|
||||
setContextMenuPosition({ x: e.clientX, y: e.clientY });
|
||||
setContextMenuTarget(e.target as HTMLElement);
|
||||
}, [isMenuDisabled, shouldDisableOnLink, contextMenuPosition]);
|
||||
|
||||
const handleContextMenuClose = useCallback(() => {
|
||||
@ -151,6 +153,7 @@ const useContextMenuHandlers = (
|
||||
return {
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
contextMenuTarget,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
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 { IS_IOS } from '../util/windowEnvironment';
|
||||
|
||||
import { ElectronEvent } from '../types/electron';
|
||||
|
||||
type RefType = {
|
||||
current: HTMLVideoElement | null;
|
||||
};
|
||||
@ -79,11 +81,16 @@ export const useFullscreenStatus = () => {
|
||||
setIsFullscreen(checkIfFullscreen());
|
||||
};
|
||||
|
||||
const removeElectronListener = window.electron?.on(ElectronEvent.FULLSCREEN_CHANGE, setIsFullscreen);
|
||||
window.electron?.isFullscreen().then(setIsFullscreen);
|
||||
|
||||
document.addEventListener('fullscreenchange', listener, false);
|
||||
document.addEventListener('webkitfullscreenchange', listener, false);
|
||||
document.addEventListener('mozfullscreenchange', listener, false);
|
||||
|
||||
return () => {
|
||||
removeElectronListener?.();
|
||||
|
||||
document.removeEventListener('fullscreenchange', listener, false);
|
||||
document.removeEventListener('webkitfullscreenchange', 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
|
||||
const cacheApi = self.caches;
|
||||
|
||||
@ -24,7 +26,9 @@ export async function fetch(
|
||||
|
||||
try {
|
||||
// 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 response = await cache.match(request);
|
||||
if (!response) {
|
||||
@ -82,7 +86,9 @@ export async function save(cacheName: string, key: string, data: AnyLiteral | Bl
|
||||
? data
|
||||
: JSON.stringify(data);
|
||||
// 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 cache = await cacheApi.open(cacheName);
|
||||
await cache.put(request, response);
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
} from '../api/types';
|
||||
|
||||
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';
|
||||
import { callApi, cancelApiProgress } from '../api/gramjs';
|
||||
import * as cacheApi from './cacheApi';
|
||||
@ -27,7 +27,7 @@ const asCacheApiType = {
|
||||
[ApiMediaFormat.Stream]: undefined,
|
||||
};
|
||||
|
||||
const PROGRESSIVE_URL_PREFIX = './progressive/';
|
||||
const PROGRESSIVE_URL_PREFIX = `${IS_ELECTRON ? ELECTRON_HOST_URL : '.'}/progressive/`;
|
||||
const URL_DOWNLOAD_PREFIX = './download/';
|
||||
|
||||
const memoryCache = new Map<string, ApiPreparedMedia>();
|
||||
|
||||
@ -4,7 +4,9 @@ import type {
|
||||
} from '../api/types';
|
||||
import { ApiMediaFormat } from '../api/types';
|
||||
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 {
|
||||
getChatAvatarHash,
|
||||
@ -44,7 +46,8 @@ function getDeviceToken(subscription: PushSubscription) {
|
||||
}
|
||||
|
||||
function checkIfPushSupported() {
|
||||
if (!IS_SERVICE_WORKER_SUPPORTED) return false;
|
||||
if (!IS_SERVICE_WORKER_SUPPORTED || IS_ELECTRON) return false;
|
||||
|
||||
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -70,6 +73,7 @@ function checkIfPushSupported() {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -446,7 +450,7 @@ export async function notifyAboutMessage({
|
||||
if (!checkIfShouldNotify(chat, message)) return;
|
||||
const areNotificationsSupported = checkIfNotificationsSupported();
|
||||
if (!hasWebNotifications || !areNotificationsSupported) {
|
||||
if (!message.isSilent && !isReaction) {
|
||||
if (!message.isSilent && !isReaction && !IS_ELECTRON) {
|
||||
// Only play sound if web notifications are disabled
|
||||
playNotifySoundDebounced(String(message.id) || chat.id);
|
||||
}
|
||||
@ -520,7 +524,7 @@ export async function notifyAboutMessage({
|
||||
// Play sound when notification is displayed
|
||||
notification.onshow = () => {
|
||||
// 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);
|
||||
};
|
||||
}
|
||||
|
||||
@ -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 { hasStoredSession } from './sessions';
|
||||
|
||||
@ -25,7 +27,7 @@ const saveSync = (authed: boolean) => {
|
||||
let lastTimeout: NodeJS.Timeout | undefined;
|
||||
|
||||
export const forceWebsync = (authed: boolean) => {
|
||||
if (IS_MOCKED_CLIENT) return undefined;
|
||||
if (IS_MOCKED_CLIENT || IS_ELECTRON) return undefined;
|
||||
const currentTs = getTs();
|
||||
|
||||
const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
|
||||
@ -67,13 +69,13 @@ export const forceWebsync = (authed: boolean) => {
|
||||
};
|
||||
|
||||
export function stopWebsync() {
|
||||
if (DEBUG) return;
|
||||
if (DEBUG || IS_ELECTRON) return;
|
||||
|
||||
if (lastTimeout) clearTimeout(lastTimeout);
|
||||
}
|
||||
|
||||
export function startWebsync() {
|
||||
if (DEBUG) {
|
||||
if (DEBUG || IS_ELECTRON) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
IS_TEST,
|
||||
IS_ELECTRON,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
VIDEO_MOV_TYPE,
|
||||
CONTENT_TYPES_WITH_PREVIEW,
|
||||
@ -37,6 +38,8 @@ export function getPlatform() {
|
||||
export const IS_PRODUCTION_HOST = window.location.host === PRODUCTION_HOSTNAME;
|
||||
export const PLATFORM_ENV = getPlatform();
|
||||
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_ANDROID = PLATFORM_ENV === 'Android';
|
||||
export const IS_MOBILE = IS_IOS || IS_ANDROID;
|
||||
@ -57,6 +60,8 @@ export const IS_PWA = (
|
||||
|| 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_VOICE_RECORDING_SUPPORTED = Boolean(
|
||||
window.navigator.mediaDevices && 'getUserMedia' in window.navigator.mediaDevices && (
|
||||
|
||||
@ -26,7 +26,8 @@
|
||||
"tests",
|
||||
"plugins",
|
||||
"dev",
|
||||
"webpack.config.js",
|
||||
"webpack.config.ts",
|
||||
"webpack-electron.config.ts",
|
||||
"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');
|
||||
const fs = require('fs');
|
||||
const dotenv = require('dotenv');
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
const {
|
||||
import {
|
||||
DefinePlugin,
|
||||
EnvironmentPlugin,
|
||||
ProvidePlugin,
|
||||
ContextReplacementPlugin,
|
||||
NormalModuleReplacementPlugin,
|
||||
} = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { GitRevisionPlugin } = require('git-revision-webpack-plugin');
|
||||
const StatoscopeWebpackPlugin = require('@statoscope/webpack-plugin').default;
|
||||
const WebpackContextExtension = require('./dev/webpackContextExtension');
|
||||
const appVersion = require('./package.json').version;
|
||||
} from 'webpack';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import { GitRevisionPlugin } from 'git-revision-webpack-plugin';
|
||||
import StatoscopeWebpackPlugin from '@statoscope/webpack-plugin';
|
||||
|
||||
import type { Configuration, Compiler } from 'webpack';
|
||||
import 'webpack-dev-server';
|
||||
|
||||
import { version as appVersion } from './package.json';
|
||||
|
||||
const {
|
||||
HEAD,
|
||||
APP_ENV = 'production',
|
||||
APP_MOCKED_CLIENT = '',
|
||||
IS_ELECTRON,
|
||||
} = process.env;
|
||||
|
||||
dotenv.config();
|
||||
@ -33,7 +37,10 @@ const {
|
||||
APP_TITLE = DEFAULT_APP_TITLE,
|
||||
} = process.env;
|
||||
|
||||
module.exports = (_env, { mode = 'production' }) => {
|
||||
export default function createConfig(
|
||||
_: any,
|
||||
{ mode = 'production' }: { mode: 'none' | 'development' | 'production' },
|
||||
): Configuration {
|
||||
return {
|
||||
mode,
|
||||
entry: './src/index.tsx',
|
||||
@ -146,13 +153,13 @@ module.exports = (_env, { mode = 'production' }) => {
|
||||
|
||||
plugins: [
|
||||
...(APP_ENV === 'staging' ? [{
|
||||
apply: (compiler) => {
|
||||
apply: (compiler: Compiler) => {
|
||||
compiler.hooks.compile.tap('Before Compilation', async () => {
|
||||
try {
|
||||
const stats = await fetch(STATOSCOPE_REFERENCE_URL).then((res) => res.text());
|
||||
fs.writeFileSync(path.resolve('./public/reference.json'), stats);
|
||||
isReferenceFetched = true;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
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
|
||||
APP_NAME: null,
|
||||
APP_VERSION: appVersion,
|
||||
IS_ELECTRON: false,
|
||||
APP_TITLE,
|
||||
RELEASE_DATETIME: Date.now(),
|
||||
TELEGRAM_T_API_ID: undefined,
|
||||
@ -212,14 +220,14 @@ module.exports = (_env, { mode = 'production' }) => {
|
||||
saveStatsTo: path.resolve('./public/build-stats.json'),
|
||||
normalizeStats: true,
|
||||
open: 'file',
|
||||
extensions: [new WebpackContextExtension()],
|
||||
extensions: [new WebpackContextExtension()], // eslint-disable-line @typescript-eslint/no-use-before-define
|
||||
...(APP_ENV === 'staging' && isReferenceFetched && {
|
||||
additionalStats: ['./public/reference.json'],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
|
||||
devtool: 'source-map',
|
||||
devtool: APP_ENV === 'production' && IS_ELECTRON ? undefined : 'source-map',
|
||||
|
||||
...(APP_ENV !== 'production' && {
|
||||
optimization: {
|
||||
@ -227,11 +235,30 @@ module.exports = (_env, { mode = 'production' }) => {
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getGitMetadata() {
|
||||
const gitRevisionPlugin = new GitRevisionPlugin();
|
||||
const branch = HEAD || gitRevisionPlugin.branch();
|
||||
const commit = gitRevisionPlugin.commithash().substring(0, 7);
|
||||
const commit = gitRevisionPlugin.commithash()?.substring(0, 7);
|
||||
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