Replace Electron with Tauri

This commit is contained in:
Alexander Zinchuk 2025-09-19 14:25:30 +02:00
parent d57a572ff8
commit 5e32fad59e
142 changed files with 11833 additions and 7053 deletions

19
.eslintignore Normal file
View File

@ -0,0 +1,19 @@
src/lib/rlottie/rlottie-wasm.js
src/lib/video-preview/polyfill
src/lib/fasttextweb/fasttext-wasm.js
src/lib/gramjs/tl/types-generator/template.ts
src/lib/gramjs/tl/api.d.ts
src/lib/gramjs/tl/apiTl.ts
src/lib/gramjs/tl/schemaTl.ts
src/lib/lovely-chart
src/lib/music-metadata-browser
jest.config.js
src/lib/secret-sauce/
playwright.config.ts
dist
public

View File

@ -4,23 +4,135 @@
# "publish" - Send a package to corresponding store and GitHub release page.
# "release" - build + package + publish
#
# Jobs in this workflow will skip the "publish" step when `PUBLISH_REPO` is not set.
# Jobs in this workflow will skip the "publish" step when `SHOULD_PUBLISH` is not set.
name: Package and publish
on:
workflow_dispatch:
inputs:
forceRelease:
description: 'Force production build'
required: false
default: false
type: boolean
push:
branches:
- master
env:
APP_NAME: Telegram A
IS_ON_MASTER: ${{ github.ref == 'refs/heads/master' }}
SHOULD_PUBLISH: ${{ github.ref == 'refs/heads/master' && vars.PUBLISH_REPO || '' }}
PUBLISH_REPO: ${{ vars.PUBLISH_REPO }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
UPDATER_GIST_URL: ${{ secrets.UPDATER_GIST_URL }}
UPDATER_GIST_ID: ${{ secrets.UPDATER_GIST_ID }}
jobs:
electron-release:
name: Build, package and publish Electron
runs-on: macOS-latest
get-version:
runs-on: ubuntu-latest
outputs:
package-version: ${{ steps.extract-version.outputs.package-version }}
tag-name: ${{ steps.extract-version.outputs.tag-name }}
should-publish: ${{ steps.extract-version.outputs.should-publish }}
release-name: ${{ steps.extract-version.outputs.release-name }}
steps:
- uses: actions/checkout@v4
- name: Extract version and tag
id: extract-version
run: |
PACKAGE_VERSION=$(grep -m1 '^version' tauri/Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
TAG_NAME="Tauri v${PACKAGE_VERSION}"
echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
echo "tag-name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "should-publish=$SHOULD_PUBLISH" >> $GITHUB_OUTPUT
echo "Extracted version: $PACKAGE_VERSION"
echo "Generated tag: $TAG_NAME"
echo "Generated release name: $RELEASE_NAME"
check-version:
runs-on: ubuntu-latest
needs: get-version
outputs:
should-skip: ${{ steps.check-release.outputs.should-skip }}
steps:
- name: Check if release already exists
id: check-release
env:
PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }}
TAG_NAME: ${{ needs.get-version.outputs.tag-name }}
run: |
# For non-master branches or when publishing is disabled, always continue
if [ -z "$SHOULD_PUBLISH" ]; then
echo "🚧 Publishing disabled (non-master branch or PUBLISH_REPO not set)"
echo "should-skip=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Checking if release already exists for tag: $TAG_NAME"
RESPONSE=$(curl -s -H "Authorization: token $GH_TOKEN" \
"https://api.github.com/repos/$PUBLISH_REPO/releases/tags/$TAG_NAME")
if echo "$RESPONSE" | jq -e '.tag_name' > /dev/null; then
IS_DRAFT=$(echo "$RESPONSE" | jq -r '.draft')
if [ "$IS_DRAFT" = "false" ]; then
echo "✅ Published release already exists for version $PACKAGE_VERSION"
echo "should-skip=true" >> $GITHUB_OUTPUT
else
echo "📝 Draft release exists for version $PACKAGE_VERSION, will continue"
echo "should-skip=false" >> $GITHUB_OUTPUT
fi
else
echo "🆕 No release found for version $PACKAGE_VERSION, will create new release"
echo "should-skip=false" >> $GITHUB_OUTPUT
fi
create-release:
runs-on: ubuntu-latest
needs: [get-version, check-version]
if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true'
outputs:
releaseId: ${{ steps.create-release.outputs.releaseId }}
steps:
- name: Create draft release
id: create-release
env:
PACKAGE_VERSION: ${{ needs.get-version.outputs.package-version }}
TAG_NAME: ${{ needs.get-version.outputs.tag-name }}
run: |
echo "Creating draft release for tag: $TAG_NAME"
echo "Repository: $PUBLISH_REPO"
RESPONSE=$(curl -X POST \
-H "Authorization: token $GH_TOKEN" \
-d '{"tag_name": "'"$TAG_NAME"'", "name": "'"$TAG_NAME"'", "draft": true}' \
"https://api.github.com/repos/$PUBLISH_REPO/releases")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Extracted Release ID: $RELEASE_ID"
if [ "$RELEASE_ID" = "null" ]; then
echo "Error: Failed to create release. Response was: $RESPONSE"
exit 1
fi
echo "releaseId=$RELEASE_ID" >> $GITHUB_OUTPUT
package-tauri:
name: Build, package and publish Tauri
needs: [get-version, check-version, create-release]
if: ${{ always() && needs.check-version.outputs.should-skip != 'true' }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
- platform: "macos-latest"
args: "--target x86_64-apple-darwin"
- platform: 'windows-latest'
args: ''
runs-on: ${{ matrix.settings.platform }}
steps:
- name: Checkout
uses: actions/checkout@v4
@ -30,6 +142,17 @@ jobs:
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install Tauri dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Cache node modules
id: npm-cache
uses: actions/cache@v4
@ -41,89 +164,106 @@ jobs:
- name: Install dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci --include=dev # Hack: install `electron-drag-click` as a dev dependency, so build happens only on macOS
run: npm ci
- name: Import MacOS signing certificate
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Extract repository owner and name
id: repository-info
if: needs.get-version.outputs.should-publish != ''
shell: bash
run: |
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > $CERTIFICATE_P12
security create-keychain -p actions $KEY_CHAIN
security default-keychain -s $KEY_CHAIN
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $APPLE_CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions $KEY_CHAIN
security find-identity -v -p codesigning $KEY_CHAIN
echo "owner=${PUBLISH_REPO%%/*}" >> $GITHUB_OUTPUT
echo "repo=${PUBLISH_REPO#*/}" >> $GITHUB_OUTPUT
- name: Get branch name for current workflow run
id: branch-name
uses: tj-actions/branch-names@v8
- name: Define Tauri configuration overrides
id: config-overrides
uses: actions/github-script@v7
env:
BASE_URL: ${{ vars.BASE_URL }}
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY }}
WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }}
with:
script: |
const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/');
const moduleUrl = `file:///${workspacePath}/deploy/prepareTauriConfig.js`;
const { default: prepareTauriConfig } = await import(moduleUrl)
const config = prepareTauriConfig();
const configJson = JSON.stringify(config);
console.log(configJson);
core.setOutput("json", configJson);
- name: Build, package and publish
uses: tauri-apps/tauri-action@v0
id: build-tauri
env:
TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }}
TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLISH_REPO: ${{ vars.PUBLISH_REPO }}
GITHUB_TOKEN: ${{ env.GH_TOKEN }}
BASE_URL: ${{ vars.BASE_URL }}
IS_PREVIEW: ${{ steps.branch-name.outputs.current_branch != 'master' }}
run: |
if [ -z "$PUBLISH_REPO" ]; then
npm run electron:package:staging
else
npm run electron:release:production
fi
- uses: actions/upload-artifact@v4
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.UPDATER_PRIVATE_KEY_PASSWORD }}
WITH_UPDATER: ${{ needs.get-version.outputs.should-publish != '' && 'true' || 'false' }}
with:
name: ${{ env.APP_NAME }}-x64.dmg
path: dist-electron/${{ env.APP_NAME }}-x64.dmg
args: "-c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}"
includeDebug: ${{ needs.get-version.outputs.should-publish == '' && !inputs.forceRelease }}
includeRelease: ${{ needs.get-version.outputs.should-publish != '' || inputs.forceRelease }}
releaseId: ${{ needs.create-release.outputs.releaseId }}
owner: ${{ steps.repository-info.outputs.owner }}
repo: ${{ steps.repository-info.outputs.repo }}
- uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-arm64.dmg
path: dist-electron/${{ env.APP_NAME }}-arm64.dmg
- uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-x86_64.AppImage
path: dist-electron/${{ env.APP_NAME }}-x86_64.AppImage
- uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-x64.exe
path: dist-electron/${{ env.APP_NAME }}-x64.exe
electron-sign-for-windows:
name: Sign and re-publish Windows package
needs: electron-release
runs-on: windows-latest
if: vars.PUBLISH_REPO != ''
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PUBLISH_REPO: ${{ vars.PUBLISH_REPO }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup certificate
shell: bash
run: echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
- name: Set environment variables
id: variables
- name: Get file info
id: file-info
shell: bash
run: |
FULL_PATH=$(echo "${{ fromJSON(steps.build-tauri.outputs.artifactPaths)[0] }}")
FILENAME=$(basename "$FULL_PATH")
NAME="${FILENAME%.*}"
FILE_PATH=$(readlink -f "$(dirname "$FULL_PATH")")
ARCHITECTURE=$(echo "${{ matrix.settings.args }}" | grep -oE 'x86_64|aarch64' || echo "")
echo "name=$NAME" >> $GITHUB_OUTPUT
echo "filename=$FILENAME" >> $GITHUB_OUTPUT
echo "architecture=$ARCHITECTURE" >> $GITHUB_OUTPUT
echo "path=$FILE_PATH" >> $GITHUB_OUTPUT
# MacOS release
- name: Rebuild DMG with custom background (MacOS)
if: matrix.settings.platform == 'macos-latest'
run: |
brew install create-dmg
./deploy/tauri_create_dmg.sh "${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.name }}.dmg" "${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}"
- name: Upload release asset (MacOS)
if: matrix.settings.platform == 'macos-latest' && needs.get-version.outputs.should-publish != ''
shell: bash
run: |
SANITIZED_FILENAME=$(echo "${{ steps.file-info.outputs.name }}" | sed 's/ /./g')
PUBLISH_FILE_NAME="$SANITIZED_FILENAME-${{ steps.file-info.outputs.architecture }}.dmg"
FILE_PATH="${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.name }}.dmg"
RELEASE_ID="${{ needs.create-release.outputs.releaseId }}"
curl -X POST -H "Authorization: Bearer $GH_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$FILE_PATH" \
"https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME"
- name: Upload artifact (MacOS)
if: matrix.settings.platform == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ steps.file-info.outputs.name }}-${{ steps.file-info.outputs.architecture }}.dmg
path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.name }}.dmg
# Windows release
- name: Setup certificate and set environment variables (Windows)
if: matrix.settings.platform == 'windows-latest'
shell: bash
run: |
echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
echo "FILE_NAME=${{ env.APP_NAME }}-x64.exe" >> "$GITHUB_ENV"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
@ -132,9 +272,12 @@ jobs:
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
echo "C:\Program Files\DigiCert\DigiCert One Signing Manager Tools" >> $GITHUB_PATH
- name: Setup SSM KSP
- name: Setup SSM KSP and sign package (Windows)
if: matrix.settings.platform == 'windows-latest'
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }}
FILE_PATH: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}
shell: cmd
run: |
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools.msi
@ -143,57 +286,74 @@ jobs:
smctl.exe keypair ls
C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
smctl.exe sign --keypair-alias=%KEYPAIR_ALIAS% --input "%FILE_PATH%"
- name: Download Windows package
id: download-artifact
uses: actions/download-artifact@v4
with:
name: ${{ env.FILE_NAME }}
- name: Sign package
env:
KEYPAIR_ALIAS: ${{ secrets.KEYPAIR_ALIAS }}
FILE_PATH: ${{ steps.download-artifact.outputs.download-path }}
shell: cmd
run: smctl.exe sign --keypair-alias=%KEYPAIR_ALIAS% --input "%FILE_PATH%\%FILE_NAME%"
- uses: actions/upload-artifact@v4
with:
name: ${{ env.FILE_NAME }}
path: ${{ env.FILE_NAME }}
overwrite: true
- name: Get latest release ID
id: release-id
- name: Update release asset (Windows)
if: matrix.settings.platform == 'windows-latest' && needs.get-version.outputs.should-publish != ''
shell: bash
run: |
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases?per_page=1" | jq -r '.[0].id')
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
PUBLISH_FILE_NAME=$(echo "${{ steps.file-info.outputs.filename }}" | sed 's/ /./g')
FILE_PATH="${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}"
RELEASE_ID="${{ needs.create-release.outputs.releaseId }}"
echo "Updating release asset for file: $PUBLISH_FILE_NAME"
echo "File path: $FILE_PATH"
echo "Release ID: $RELEASE_ID"
echo "Repository: $PUBLISH_REPO"
- name: Delete existing asset
env:
RELEASE_ID: ${{ steps.release-id.outputs.release_id }}
shell: bash
run: |
PUBLISH_FILE_NAME=${FILE_NAME// /-} # Consistency with electron-builder
ASSET_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets" | jq -r --arg PUBLISH_FILE_NAME "$PUBLISH_FILE_NAME" '.[] | select(.name == $PUBLISH_FILE_NAME) | .id')
curl -X DELETE -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID"
echo "Fetching existing assets..."
ASSETS_RESPONSE=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets")
echo "Assets API Response:"
echo "$ASSETS_RESPONSE"
- name: Push new asset
env:
FILE_PATH: ${{ steps.download-artifact.outputs.download-path }}
RELEASE_ID: ${{ steps.release-id.outputs.release_id }}
shell: bash
run: |
PUBLISH_FILE_NAME=${FILE_NAME// /-} # Consistency with electron-builder
curl -X POST -H "Authorization: Bearer $GH_TOKEN" \
ASSET_ID=$(echo "$ASSETS_RESPONSE" | jq -r --arg PUBLISH_FILE_NAME "$PUBLISH_FILE_NAME" '.[] | select(.name == $PUBLISH_FILE_NAME) | .id')
echo "Found Asset ID: $ASSET_ID"
if [ "$ASSET_ID" != "null" ] && [ "$ASSET_ID" != "" ]; then
echo "Deleting existing asset with ID: $ASSET_ID"
DELETE_RESPONSE=$(curl -w "HTTP_STATUS:%{http_code}" -s -X DELETE -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID")
echo "Delete response: $DELETE_RESPONSE"
else
echo "No existing asset found to delete"
fi
echo "Uploading new asset..."
UPLOAD_RESPONSE=$(curl -w "HTTP_STATUS:%{http_code}" -s -X POST -H "Authorization: Bearer $GH_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$FILE_PATH\\$FILE_NAME" \
"https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME"
--data-binary "@$FILE_PATH" \
"https://uploads.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets?name=$PUBLISH_FILE_NAME")
echo "Upload response: $UPLOAD_RESPONSE"
- name: Upload Windows artifact (Windows)
if: matrix.settings.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ steps.file-info.outputs.filename }}
path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}
# Linux release
- name: Upload Linux artifact (Linux)
if: matrix.settings.platform == 'ubuntu-22.04'
uses: actions/upload-artifact@v4
with:
name: ${{ steps.file-info.outputs.filename }}
path: ${{ steps.file-info.outputs.path }}/${{ steps.file-info.outputs.filename }}
publish-release:
runs-on: ubuntu-latest
needs: [get-version, check-version, create-release, package-tauri]
if: needs.get-version.outputs.should-publish != '' && needs.check-version.outputs.should-skip != 'true'
env:
RELEASE_ID: ${{ needs.create-release.outputs.releaseId }}
steps:
- uses: actions/checkout@v4
- name: Publish release
env:
RELEASE_ID: ${{ steps.release-id.outputs.release_id }}
shell: bash
run: |
curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID" -d '{"draft": false}'
curl -X PATCH -H "Authorization: Bearer $GH_TOKEN" -d '{"draft": false}' "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID"
- name: Update Gist with JSON
run: |
ASSET_ID=$(curl -s -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/$RELEASE_ID/assets" | jq -r '.[] | select(.name == "latest.json") | .id')
JSON_CONTENT=$(curl -sSL -H "Accept: application/octet-stream" -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$PUBLISH_REPO/releases/assets/$ASSET_ID")
GIST_CONTENT=$(jq -n --arg json "$JSON_CONTENT" '{"files":{"updater.json":{"content":$json}}}')
curl -X PATCH -H "Authorization: token $GH_TOKEN" -d "$GIST_CONTENT" "https://api.github.com/gists/$UPDATER_GIST_ID"

View File

@ -37,77 +37,6 @@ 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: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: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 deploy:electron`
Create packages for macOS, Windows and Linux in `dist-electron` folder and publish release to GitHub, which allows supporting autoupdates. See [GitHub release workflow](#github-release) for more info.
#### 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 automatically in [electron-builder](https://github.com/electron-userland/electron-builder/) module, which requires `APPLE_ID` and `APPLE_APP_SPECIFIC_PASSWORD` environment variables to be passed.
How to obtain app-specific password:
- 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.
### Dependencies
* [GramJS](https://github.com/gram-js/gramjs) ([MIT License](https://github.com/gram-js/gramjs/blob/master/LICENSE))
* [pako](https://github.com/nodeca/pako) ([MIT License](https://github.com/nodeca/pako/blob/master/LICENSE))

View File

@ -0,0 +1,25 @@
/* eslint-disable no-null/no-null */
export default function prepareTauriConfig() {
const config = {
build: {
frontendDist: process.env.BASE_URL,
devUrl: null,
},
};
if (process.env.WITH_UPDATER === 'true') {
config.plugins = {
updater: {
dialog: false,
endpoints: [process.env.UPDATER_GIST_URL],
pubkey: process.env.UPDATER_PUBLIC_KEY,
},
};
config.bundle = {
createUpdaterArtifacts: true,
};
}
return config;
}

13
deploy/tauri_create_dmg.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
create-dmg \
--volname "Telegram Air installer" \
--volicon "./tauri/icons/icon.icns" \
--background "./tauri/images/background-dmg.tiff" \
--window-size 540 380 \
--icon-size 100 \
--icon "Telegram Air.app" 138 225 \
--hide-extension "Telegram Air.app" \
--app-drop-link 402 225 \
"$1" \
"$2"

184
docs/TAURI.md Normal file
View File

@ -0,0 +1,184 @@
# Tauri
**Tauri** allows building a native application that can be installed on Windows, macOS and Linux.
Since it's based on native OS WebView, you must compile application separately for each target platform, meaning you cannot build a macOS application on a Windows machine, or vice versa. Each build must be done on its respective platform.
## Table of contents
- [Installation](#installation)
- [Upgrading dependencies](#upgrading-dependencies)
- [NPM scripts](#npm-scripts)
- [Implementation specifics](#implementation-specifics)
- [Accessing the Tauri API](#accessing-the-tauri-api)
- [Custom header on MacOS](#custom-header-on-macos)
- [Multiple windows support](#multiple-windows-support)
- [Notifications](#notifications)
- [Browser devtool](#browser-devtools)
- [Capabilities](#capabilities)
- [Autoupdates](#autoupdates)
- [GitHub workflow for release](#github-workflow-for-release)
- [Important links](#important-links)
## Installation
To run Tauri locally, ensure that [Rust is installed](https://tauri.app/start/prerequisites/#rust).
## Upgrading dependencies
- To detect available upgrades for NPM modules, run:
```bash
# Get outdated module
npm outdated @tauri-apps/{MODULE} # e.g. npm outdated @tauri-apps/cli
# or list all available versions
npm view @tauri-apps/{MODULE} versions -json
# Install a specific version
npm install @tauri-apps/cli@{VERSION}
# or install the latest version
npm install @tauri-apps/cli@latest
```
- To upgrade Rust (Cargo) modules, run:
```bash
# Install the cargo-edit module for easier upgrade
cargo install cargo-edit
# Change to the `/tauri` directory
cd tauri
# Run the upgrade
cargo upgrade
```
For details on upgrading Tauri dependencies, refer to the [official documentation](https://tauri.app/develop/updating-dependencies/).
## NPM scripts
- `npm run tauri:dev` — run Tauri in development mode.
- `npm run tauri` — placeholder, which allows you to run [Tauri CLI](https://v2.tauri.app/reference/cli/) commands with `npm run tauri {COMMAND}`.
## Implementation specifics
### Accessing the Tauri API
The Tauri API, including any integrated plugins, is accessible via the `window.tauri` object. For type definitions, refer to the `src/types/tauri.ts` file.
If you have implemented [custom commands](https://tauri.app/develop/calling-rust/), ensure they are properly registered in the `src/util/tauri/initTauriApi.ts`
### Custom header on MacOS
Tauri currently has [some problems](https://github.com/tauri-apps/tauri/issues/13044) with custom titlebar style. Current implementation that uses native window handle would be removed when those problems are fixed.
### Multiple windows support
The Tauri main process exposes the `open_new_window` command, available as `window.tauri.openNewWindow`. This method can be used to open a new "child" closable window:
```typescript
openNewWindow: (url: string) => Promise<void>
```
### Notifications
The Tauri notifications plugin [overrides the default Notification web API](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/guest-js/init.ts#L56), so no additional function needs to be called to send a notification to the user.
**Important:**
- Clicking on notifications to open the appropriate chat is currently not possible. More details in the [issue](https://github.com/tauri-apps/plugins-workspace/issues/1903).
### Browser devtools
Browser DevTools context menu can be enabled by adding the `devtools` feature to the `tauri/Cargo.toml` file:
```rust
[dependencies]
tauri = { version = "...", features = ["...", "devtools"] }
```
For debug builds, DevTools are included by default through the `includeDebug` flag in the GitHub action. [More info](https://tauri.app/develop/debug/#webview-console)
### Capabilities
The `tauri/capabilities` folder provides fine-grained control over application windows and access to Tauri core, application, or plugin commands. These capabilities can be configured for different environments such as `development`, `staging` and `production`.
Keep them at minimum. For complex logic, consider implementing own command in Rust, rather than giving permissions to JS side.
Learn more about [capabilities](https://tauri.app/reference/acl/capability/) and [how to configure them for different windows or platforms](https://tauri.app/learn/security/capabilities-for-windows-and-platforms/).
## Autoupdates
The application's autoupdate cycle is managed using the [Updater](https://tauri.app/plugin/updater/) plugin.
Each time the "Package & Publish" GitHub workflow runs successfully, a new release is created in the publish repository. This release includes build artifacts and a `latest.json` file with a [JSON file](https://tauri.app/plugin/updater/#static-json-file) containing download links for each platform and signature tokens.
The frontend application polls for updates every 10 minutes and displays an "Update" button if an update is available.
**Important**: In development mode and for local builds, autoupdates are not available. Keys and other information are dynamically added within the GitHub action during the `Define Tauri configuration overrides` step.
## GitHub workflow for release
The build and release process for a Tauri application is managed using a GitHub workflow that leverages the official [Tauri Action](https://github.com/tauri-apps/tauri-action).
### List of variables and secrets
### Variables
| **Variable Name** | **Description** |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------|
| `PUBLISH_REPO_TAURI` | `{OWNER}/{REPO}` repository where published releases with artifacts will be pushed. |
| `NODE_VERSION` | Node.js version on which NPM modules installation and Tauri build should happen. |
| `BASE_URL_TAURI` | Remote URL from which application content will be loaded instead of the local index.html file, if the Auto-Updates feature is enabled in user settings. |
| `WITH_UPDATER` | Include `updater` plugin. Check Secrets section for the required env parameters
---
### Secrets - Generic
| **Secret Name** | **Description** |
|------------------|--------------------------------------------------------------------------------------|
| `GH_TOKEN_TAURI` | GitHub access token with `repo` scope/permission, required to publish new releases. |
---
### Secrets - Application Updates
| **Secret Name** | **Description** |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------|
| `UPDATER_GIST_URL` | URL to GitHub gist (e.g., `https://gist.githubusercontent.com/GitHubUser/GistID/raw/updater.json`). Ensure `GH_TOKEN_TAURI` has read/write access. |
| `UPDATER_GIST_ID` | GitHub gist ID (`GistID` from `UPDATER_GIST_URL` example). |
| `UPDATER_PUBLIC_KEY` | Public key to validate artifacts before installation. [More info](https://tauri.app/plugin/updater/#signing-updates). |
| `UPDATER_PRIVATE_KEY` | Private key used to sign installer files (generated with the same command as public key). |
---
### Secrets - MacOS Signing
| **Secret Name** | **Description** |
|------------------------------|-------------------------------------------------------------------------------------|
| `APPLE_CERTIFICATE_BASE64` | Base64 string of the `.p12` certificate, exported from the keychain. |
| `APPLE_CERTIFICATE_PASSWORD` | Password for the `.p12` certificate. |
| `APPLE_SIGNING_IDENTITY` | Name of the keychain entry that contains the signing certificate. |
| `APPLE_ID` | Apple account email. |
| `APPLE_APP_SPECIFIC_PASSWORD`| Apple account [app-specific password](https://support.apple.com/en-ca/102654). |
| `APPLE_TEAM_ID` | Apple account [team ID](https://developer.apple.com/account#MembershipDetailsCard). |
---
### Secrets - Windows Signing
| **Secret Name** | **Description** |
|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `SM_CLIENT_CERT_FILE_B64` | Base64 encoded version of the [authentication certificate](https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html#create-an-authentication-certificate-426026). |
| `SM_CLIENT_CERT_PASSWORD` | Password for the authentication certificate. |
| `SM_HOST` | [Path to the DigiCert ONE portal with client authorization](https://docs.digicert.com/en/software-trust-manager/general/requirements.html#host-environment-367442). |
| `SM_API_KEY` | [API token](https://docs.digicert.com/en/software-trust-manager/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html#create-an-api-token-426026) created with the authentication certificate. |
| `KEYPAIR_ALIAS` | Keypair alias for the [certificate keylocker](https://one.digicert.com/signingmanager/certificates-keylocker). |
## Important links
- [Rust documentation for Tauri](https://docs.rs/tauri/latest/tauri/)
- [Plugins documentation](https://tauri.app/plugin/)
- [Plugins GitHub repository](https://github.com/tauri-apps/plugins-workspace)

View File

@ -39,7 +39,6 @@ export default tseslint.config(
'src/lib/fastBlur.js',
'src/types/language.d.ts',
'dist/',
'dist-electron/',
'public/',
'deploy/update_version.js',
]),

7554
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "telegram-t",
"version": "10.9.72",
"version": "10.9.71",
"description": "",
"type": "module",
"main": "index.js",
@ -12,12 +12,9 @@
"build:mocked": "cross-env APP_ENV=test APP_MOCKED_CLIENT=1 npm run build:dev",
"build:production": "webpack && bash ./deploy/copy_to_dist.sh",
"web:release:production": "npm i && npm run build:production && git add -A && git commit -a -m '[Build]' --no-verify && git push",
"electron:dev": "npm run electron:webpack && IS_PACKAGED_ELECTRON=true concurrently --ks SIGKILL -n main,renderer,electron \"npm run electron:webpack -- --watch\" \"npm run dev\" \"electronmon dist/electron.cjs\"",
"electron:webpack": "cross-env APP_ENV=$ENV webpack --config ./webpack-electron.config.ts",
"electron:build": "IS_PACKAGED_ELECTRON=true npm run build:$ENV && electron-builder install-app-deps && ENV=$ENV npm run electron:webpack",
"electron:package": "npm run electron:build && npx rimraf dist-electron && electron-builder build --win --mac --linux --config src/electron/config.js",
"electron:package:staging": "ENV=staging npm run electron:package -- -p never",
"electron:release:production": "ENV=production npm run electron:package -- -p always",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build",
"telegraph:update_changelog": "node ./dev/telegraphChangelog.js",
"check": "tsc && stylelint \"**/*.{css,scss}\" && eslint",
"check:fix": "stylelint \"**/*.{css,scss}\" --fix && eslint --fix",
@ -40,9 +37,6 @@
"*.{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,72 +45,64 @@
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@babel/register": "^7.27.1",
"@eslint/js": "^9.28.0",
"@eslint/js": "^9.29.0",
"@glen/jest-raw-loader": "^2.0.0",
"@mytonwallet/stylelint-whole-pixel": "github:mytonwallet-org/stylelint-whole-pixel#fd07e44d786460f7d469076b1d2cb1b05297896c",
"@mytonwallet/webpack-watch-file-plugin": "github:mytonwallet-org/webpack-watch-file-plugin#747b7fd29da9a928aa8b63299adfba461d2f1231",
"@playwright/test": "^1.52.0",
"@playwright/test": "^1.53.0",
"@statoscope/cli": "5.29.0",
"@statoscope/webpack-plugin": "5.29.0",
"@stylistic/eslint-plugin": "^4.4.0",
"@stylistic/eslint-plugin": "^4.4.1",
"@stylistic/stylelint-config": "^2.0.0",
"@stylistic/stylelint-plugin": "^3.1.2",
"@tauri-apps/cli": "^2.8.0",
"@testing-library/jest-dom": "^6.6.3",
"@twbs/fantasticon": "^3.1.0",
"@types/dom-view-transitions": "^1.0.6",
"@types/hast": "^3.0.4",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^8.33.1",
"@typescript-eslint/parser": "^8.33.1",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
"@webpack-cli/serve": "^3.0.1",
"autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"bindings": "git+https://github.com/zubiden/node-bindings#1f689378b1cd26f99d3b7156fe40a520365d1272",
"browserlist": "^1.0.1",
"buffer": "^6.0.3",
"concurrently": "^9.1.2",
"copy-webpack-plugin": "^13.0.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"dotenv": "^16.5.0",
"electron": "^36.3.2",
"electron-builder": "^26.0.12",
"electron-conf": "^1.3.0",
"electron-context-menu": "^4.1.0",
"electron-drag-click": "git+https://github.com/zubiden/electron-drag-click#cf6918ddb648e13ebcf6cf1e7aa008258edc06ad",
"electron-updater": "^6.6.2",
"electronmon": "^2.0.3",
"eslint": "^9.28.0",
"eslint": "^9.29.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.12.0",
"eslint-plugin-jest": "^28.14.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-no-null": "^1.0.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks-static-deps": "git+https://github.com/zubiden/eslint-plugin-react-hooks-static-deps#c16f35bf2e6e364cbc692c73cc350c1c5d46cc6e",
"eslint-plugin-react-x": "^1.51.0",
"eslint-plugin-react-x": "^1.52.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tt-multitab": "git+https://github.com/zubiden/eslint-plugin-tt-multitab#15d542004d39ec7c29d50385484511bab0b77ea9",
"eslint-plugin-unused-imports": "^4.1.4",
"fake-indexeddb": "^6.0.1",
"git-revision-webpack-plugin": "^5.0.0",
"gitlog": "^5.1.0",
"glob": "^11.0.2",
"glob": "^11.0.3",
"html-webpack-plugin": "^5.6.3",
"husky": "^9.1.7",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^16.1.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"lint-staged": "^16.1.2",
"mini-css-extract-plugin": "^2.9.2",
"minimatch": "^10.0.1",
"minimatch": "^10.0.3",
"postcss-loader": "^8.1.1",
"postcss-modules": "^6.0.1",
"react": "^19.1.0",
"sass": "^1.89.1",
"sass": "^1.89.2",
"sass-loader": "^16.0.5",
"script-loader": "^0.7.2",
"serve": "^14.2.4",
@ -128,21 +114,26 @@
"stylelint-high-performance-animation": "^1.11.0",
"stylelint-selector-tag-no-without-class": "^3.0.1",
"telegraph-node": "^1.0.4",
"tsx": "^4.19.4",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.33.1",
"typescript-eslint": "^8.34.1",
"webpack": "^5.99.9",
"webpack-dev-server": "^5.2.2"
},
"dependencies": {
"@cryptography/aes": "^0.1.1",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.9.0",
"async-mutex": "^0.5.0",
"big-integer": "github:painor/BigInteger.js",
"emoji-data-ios": "git+https://github.com/korenskoy/emoji-data-ios#443f1c9d7b16a82e7ee53f7f226d7d9a9920a105",
"idb-keyval": "^6.2.2",
"lowlight": "^3.3.0",
"mp4box": "^0.5.4",
"music-metadata": "^11.2.3",
"music-metadata": "^11.3.0",
"opus-recorder": "github:Ajaxy/opus-recorder",
"os-browserify": "^0.3.0",
"pako": "^2.1.0",
@ -152,8 +143,5 @@
"optionalDependencies": {
"dmg-license": "^1.0.11",
"fsevents": "^2.3.3"
},
"overrides": {
"bindings": "$bindings"
}
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View File

@ -1 +1 @@
10.9.72
10.9.71

View File

@ -67,7 +67,7 @@ const ABORT_CONTROLLERS = new Map<string, AbortController>();
let client: TelegramClient;
let currentUserId: string | undefined;
export async function init(initialArgs: ApiInitialArgs) {
export async function init(initialArgs: ApiInitialArgs, onConnected?: NoneToVoidFunction) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('>>> START INIT API');
@ -132,7 +132,7 @@ export async function init(initialArgs: ApiInitialArgs) {
webAuthTokenFailed: onWebAuthTokenFailed,
mockScenario,
accountIds,
});
}, onConnected);
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);

View File

@ -6,6 +6,7 @@ import type {
import type { LocalDb } from '../localDb';
import type { MethodArgs, MethodResponse, Methods } from './types';
import Deferred from '../../../util/Deferred';
import { updateFullLocalDb } from '../localDb';
import { init as initUpdateEmitter } from '../updates/apiUpdateEmitter';
import { init as initClient } from './client';
@ -16,8 +17,9 @@ export function initApi(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs, ini
if (initialLocalDb) updateFullLocalDb(initialLocalDb);
// IMPORTANT: Do not await this, or login will not work
initClient(initialArgs);
const connectDeferred = new Deferred<void>();
initClient(initialArgs, () => connectDeferred.resolve());
return connectDeferred.promise;
}
export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<T>): MethodResponse<T> {

View File

@ -6,6 +6,7 @@ import type { MethodArgs, MethodResponse, Methods } from '../methods/types';
import type { OriginPayload, ThenArg, WorkerMessageEvent } from './types';
import { DEBUG, IGNORE_UNHANDLED_ERRORS } from '../../../config';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { logDebugMessage } from '../../../util/debugConsole';
import Deferred from '../../../util/Deferred';
import { getCurrentTabId, subscribeToMasterChange } from '../../../util/establishMultitabRole';
@ -101,8 +102,8 @@ export function initApi(onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
});
subscribeToWorker(onUpdate);
if (initialArgs.platform === 'iOS') {
setupIosHealthCheck();
if (initialArgs.platform === 'iOS' || (initialArgs.platform === 'macOS' && IS_TAURI)) {
setupHealthCheck();
}
}
@ -419,7 +420,7 @@ function makeRequest(message: OriginPayload) {
const startedAt = Date.now();
// Workaround for iOS sometimes stops interacting with worker
function setupIosHealthCheck() {
function setupHealthCheck() {
window.addEventListener('focus', () => {
void ensureWorkerPing();
// Sometimes a single check is not enough

View File

@ -54,14 +54,15 @@ onmessage = ({ data }: OriginMessageEvent) => {
switch (payload.type) {
case 'initApi': {
const { messageId, args } = payload;
initApi(onUpdate, args[0], args[1]);
if (messageId) {
sendToOrigin({
type: 'methodResponse',
messageId,
response: true,
});
}
initApi(onUpdate, args[0], args[1]).then(() => {
if (messageId) {
sendToOrigin({
type: 'methodResponse',
messageId,
response: true,
});
}
});
break;
}
case 'callMethod': {

View File

@ -1118,7 +1118,6 @@
"AttachStory" = "Story";
"AttachInvoice" = "Invoice: {description}";
"AttachLocation" = "Location";
"AttachLiveLocation" = "Live Location";
"AttachGiveaway" = "Giveaway";
"AttachGiveawayResults" = "Giveaway Results";
"AttachTodo" = "Checklist";
@ -2199,6 +2198,7 @@
"ToDoListErrorChooseTasks" = "Please enter at least one task.";
"GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**";
"PremiumPreviewTodo" = "Checklists";
"NativeDownloadFailed" = "Failed to save file to the Downloads folder";
"DescriptionAboutTon" = "Offer TON to submit post suggestions to channels on Telegram.";
"ButtonTopUpViaFragment" = "Top Up Via Fragment";
"TonModalHint" = "You can top up your TON using Fragment.";

View File

@ -19,6 +19,7 @@ import { getInitialLocationHash, parseInitialLocationHash } from '../util/routin
import { checkSessionLocked, hasStoredSession } from '../util/sessions';
import { updateSizes } from '../util/windowSize';
import useTauriDrag from '../hooks/tauri/useTauriDrag';
import useAppLayout from '../hooks/useAppLayout';
import useFlag from '../hooks/useFlag';
import usePreviousDeprecated from '../hooks/usePreviousDeprecated';
@ -213,6 +214,8 @@ const App: FC<StateProps> = ({
}
}
useTauriDrag();
useLayoutEffect(() => {
document.body.classList.add(styles.bg);
}, []);

View File

@ -20,7 +20,7 @@
margin-bottom: 1.75rem;
margin-left: auto;
body.is-electron & {
body.is-tauri & {
width: 6rem;
height: 6rem;
margin-bottom: 1.75rem;
@ -60,7 +60,7 @@
}
}
body.is-electron #auth-phone-number-form & {
body.is-tauri #auth-phone-number-form & {
padding-top: 3rem;
.form {
@ -83,15 +83,6 @@
#auth-password-form,
#auth-qr-form {
overflow-y: auto;
height: 100%;
body.is-electron.is-macos & {
-webkit-app-region: drag;
.input-group {
-webkit-app-region: no-drag;
}
}
}
#auth-phone-number-form {
@ -247,6 +238,10 @@
position: absolute;
top: 1rem;
left: 1rem;
body.is-tauri & {
left: var(--window-controls-width);
}
}
@keyframes qr-show {

View File

@ -1,15 +1,15 @@
import '../../global/actions/initial';
import type { FC } from '../../lib/teact/teact';
import { memo, useRef } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { GlobalState } from '../../global/types';
import { PLATFORM_ENV } from '../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS, PLATFORM_ENV } from '../../util/browser/windowEnvironment';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useElectronDrag from '../../hooks/useElectronDrag';
import useHistoryBack from '../../hooks/useHistoryBack';
import Transition from '../ui/Transition';
@ -46,9 +46,6 @@ const Auth: FC<StateProps> = ({
onBack: handleChangeAuthorizationMethod,
});
const containerRef = useRef<HTMLDivElement>();
useElectronDrag(containerRef);
// For animation purposes
const renderingAuthState = useCurrentOrPrev(
authState !== 'authorizationStateReady' ? authState : undefined,
@ -90,7 +87,12 @@ const Auth: FC<StateProps> = ({
}
return (
<Transition activeKey={getActiveKey()} name="fade" className="Auth" ref={containerRef}>
<Transition
activeKey={getActiveKey()}
name="fade"
className="Auth"
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
>
{getScreen()}
</Transition>
);

View File

@ -8,6 +8,7 @@ import { withGlobal } from '../../global';
import type { ApiCountryCode } from '../../api/types';
import { ANIMATION_END_DELAY } from '../../config';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_EMOJI_SUPPORTED } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { isoToEmoji } from '../../util/emoji/emoji';
@ -118,6 +119,7 @@ const CountryCodeInput: FC<OwnProps & StateProps> = ({
id={id}
value={inputValue}
autoComplete="off"
spellCheck={IS_TAURI ? false : undefined}
onClick={handleTrigger}
onFocus={handleTrigger}
onInput={handleCodeInput}

View File

@ -29,12 +29,11 @@ const ActiveCallHeader: FC<StateProps> = ({
useEffect(() => {
document.body.classList.toggle('has-call-header', Boolean(isCallPanelVisible));
const updateButtonPosition = window.electron?.setWindowButtonsPosition || window.electron?.setTrafficLightPosition;
updateButtonPosition?.(isCallPanelVisible ? 'lowered' : 'standard');
window.tauri?.markTitleBarOverlay(!isCallPanelVisible);
return () => {
document.body.classList.toggle('has-call-header', false);
updateButtonPosition?.('standard');
window.tauri?.markTitleBarOverlay(true);
};
}, [isCallPanelVisible]);

View File

@ -89,8 +89,8 @@
border-bottom-color: var(--group-call-panel-header-border-color);
}
:global(body.is-electron) .root.fullscreen:not(.landscape) & {
padding-left: 5rem;
:global(body.is-tauri) .root.fullscreen:not(.landscape) & {
padding-left: var(--window-controls-width);
}
}
@ -174,8 +174,8 @@
align-items: center;
padding: 0.375rem 0.875rem;
:global(body.is-electron) .root:not(.appFullscreen) & {
padding-left: 5rem;
:global(body.is-tauri) .root:not(.appFullscreen) & {
padding-left: var(--window-controls-width);
}
}

View File

@ -63,8 +63,8 @@
color: #fff;
}
:global(body.is-electron) .root.single-column & {
padding-left: 5rem;
:global(body.is-tauri) .root.single-column & {
padding-left: var(--window-controls-width);
}
}

View File

@ -12,7 +12,7 @@ import type RLottieInstance from '../../lib/rlottie/RLottie';
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { ensureRLottie, getRLottie } from '../../lib/rlottie/RLottie.async';
import { IS_ELECTRON } from '../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import generateUniqueId from '../../util/generateUniqueId';
@ -276,7 +276,7 @@ const AnimatedSticker: FC<OwnProps> = ({
className={buildClassName('AnimatedSticker', className)}
style={buildStyle(
size !== undefined && `width: ${size}px; height: ${size}px;`,
onClick && !IS_ELECTRON && 'cursor: pointer',
onClick && !IS_TAURI && 'cursor: pointer',
colorFilter,
style,
)}

View File

@ -7,6 +7,7 @@ import {
import { MIN_PASSWORD_LENGTH } from '../../config';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import stopEvent from '../../util/stopEvent';
@ -138,6 +139,7 @@ const PasswordForm: FC<OwnProps> = ({
id="sign-in-password"
value={password || ''}
autoComplete={shouldDisablePasswordManager ? 'one-time-code' : 'current-password'}
spellCheck={IS_TAURI ? false : undefined}
onChange={onPasswordChange}
maxLength={256}
dir="auto"

View File

@ -1,10 +1,10 @@
import type { TeactNode } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { ThreadId } from '../../types';
import { ApiMessageEntityTypes } from '../../api/types';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { ensureProtocol, getUnicodeUrl, isMixedScriptUrl } from '../../util/browser/url';
import buildClassName from '../../util/buildClassName';
@ -68,7 +68,7 @@ const SafeLink = ({
<a
href={ensureProtocol(url)}
title={getUnicodeUrl(url)}
target="_blank"
target={IS_TAURI ? '_self' : '_blank'}
rel="noopener noreferrer"
className={classNames}
onClick={handleClick}

View File

@ -2,9 +2,7 @@ import type { TeactNode } from '../../../lib/teact/teact';
import type { TextPart } from '../../../types';
import {
BASE_URL, IS_PACKAGED_ELECTRON, RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE,
} from '../../../config';
import { RE_LINK_TEMPLATE, RE_MENTION_TEMPLATE } from '../../../config';
import EMOJI_REGEX from '../../../lib/twemojiRegex';
import { IS_EMOJI_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
@ -118,8 +116,7 @@ function replaceEmojis(textParts: TextPart[], size: 'big' | 'small', type: 'jsx'
if (!code) {
emojiResult.push(emoji);
} else {
const baseSrcUrl = IS_PACKAGED_ELECTRON ? BASE_URL : '.';
const src = `${baseSrcUrl}/img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
const src = `./img-apple-${size === 'big' ? '160' : '64'}/${code}.png`;
const className = buildClassName(
'emoji',
size === 'small' && 'emoji-small',

View File

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

View File

@ -45,15 +45,7 @@
}
}
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) {
body.is-tauri.is-macos #Main:not(.is-fullscreen) &:not(#TopicListHeader) {
justify-content: space-between;
padding: 0.5rem 0.5rem 0.5rem 4.5rem;

View File

@ -53,7 +53,6 @@ type StateProps = {
nextFoldersAction?: ReducerAction<FoldersActions>;
isChatOpen: boolean;
isAppUpdateAvailable?: boolean;
isElectronUpdateAvailable?: boolean;
isForumPanelOpen?: boolean;
forumPanelChatId?: string;
isClosingSearch?: boolean;
@ -90,7 +89,6 @@ function LeftColumn({
nextFoldersAction,
isChatOpen,
isAppUpdateAvailable,
isElectronUpdateAvailable,
isForumPanelOpen,
forumPanelChatId,
isClosingSearch,
@ -539,7 +537,6 @@ function LeftColumn({
onReset={handleReset}
shouldSkipTransition={shouldSkipHistoryAnimations}
isAppUpdateAvailable={isAppUpdateAvailable}
isElectronUpdateAvailable={isElectronUpdateAvailable}
isForumPanelOpen={isForumPanelOpen}
onTopicSearch={handleTopicSearch}
isAccountFrozen={isAccountFrozen}
@ -588,7 +585,6 @@ export default memo(withGlobal<OwnProps>(
hasPasscode,
},
isAppUpdateAvailable,
isElectronUpdateAvailable,
archiveSettings,
} = global;
@ -610,7 +606,6 @@ export default memo(withGlobal<OwnProps>(
nextFoldersAction,
isChatOpen,
isAppUpdateAvailable,
isElectronUpdateAvailable,
isForumPanelOpen,
forumPanelChatId,
isClosingSearch: tabState.globalSearch.isClosing,

View File

@ -21,7 +21,7 @@
font-size: 0.625rem;
font-weight: var(--font-weight-medium);
line-height: 0.75rem;
line-height: 0.875rem;
color: var(--accent-color);
text-align: center;
text-transform: uppercase;

View File

@ -1,3 +1,4 @@
import type { Update } from '@tauri-apps/plugin-updater';
import type { FC } from '../../../lib/teact/teact';
import {
memo, useEffect, useRef, useState,
@ -7,10 +8,12 @@ import { getActions } from '../../../global';
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
import { LeftColumnContent } from '../../../types';
import { PRODUCTION_URL } from '../../../config';
import { IS_ELECTRON, IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import { DEBUG } from '../../../config';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import useInterval from '../../../hooks/schedulers/useInterval';
import useForumPanelRender from '../../../hooks/useForumPanelRender';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@ -35,7 +38,6 @@ type OwnProps = {
shouldSkipTransition?: boolean;
foldersDispatch: FolderEditDispatch;
isAppUpdateAvailable?: boolean;
isElectronUpdateAvailable?: boolean;
isForumPanelOpen?: boolean;
isClosingSearch?: boolean;
onSearchQuery: (query: string) => void;
@ -46,6 +48,7 @@ type OwnProps = {
const TRANSITION_RENDER_COUNT = Object.keys(LeftColumnContent).length / 2;
const BUTTON_CLOSE_DELAY_MS = 250;
const TAURI_CHECK_UPDATE_INTERVAL = 10 * 60 * 1000;
let closeTimeout: number | undefined;
@ -58,7 +61,6 @@ const LeftMain: FC<OwnProps> = ({
shouldSkipTransition,
foldersDispatch,
isAppUpdateAvailable,
isElectronUpdateAvailable,
isForumPanelOpen,
onSearchQuery,
onReset,
@ -67,11 +69,8 @@ const LeftMain: FC<OwnProps> = ({
}) => {
const { closeForumPanel, openLeftColumnContent } = getActions();
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
const [isElectronAutoUpdateEnabled, setIsElectronAutoUpdateEnabled] = useState(false);
useEffect(() => {
window.electron?.getIsAutoUpdateEnabled().then(setIsElectronAutoUpdateEnabled);
}, []);
const [tauriUpdate, setTauriUpdate] = useState<Update>();
const [isTauriUpdateDownloading, setIsTauriUpdateDownloading] = useState(false);
const {
shouldRenderForumPanel, handleForumPanelAnimationEnd,
@ -83,7 +82,7 @@ const LeftMain: FC<OwnProps> = ({
const {
shouldRender: shouldRenderUpdateButton,
transitionClassNames: updateButtonClassNames,
} = useShowTransitionDeprecated(isAppUpdateAvailable || isElectronUpdateAvailable);
} = useShowTransitionDeprecated(isAppUpdateAvailable || Boolean(tauriUpdate));
const isMouseInside = useRef(false);
@ -123,11 +122,20 @@ const LeftMain: FC<OwnProps> = ({
closeForumPanel();
});
const handleUpdateClick = useLastCallback(() => {
if (IS_ELECTRON && !isElectronAutoUpdateEnabled) {
window.open(`${PRODUCTION_URL}/get`, '_blank', 'noopener');
} else if (isElectronUpdateAvailable) {
window.electron?.installUpdate();
const handleUpdateClick = useLastCallback(async () => {
if (tauriUpdate) {
try {
setIsTauriUpdateDownloading(true);
await tauriUpdate.downloadAndInstall();
setIsTauriUpdateDownloading(false);
await window.tauri?.relaunch();
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to download and install Tauri update', e);
} finally {
setIsTauriUpdateDownloading(false);
}
} else {
window.location.reload();
}
@ -159,6 +167,24 @@ const LeftMain: FC<OwnProps> = ({
};
}, [content]);
const checkTauriUpdate = useLastCallback(() => {
window.tauri?.checkUpdate()
.then((update) => setTauriUpdate(update ?? undefined))
.catch((e) => {
// eslint-disable-next-line no-console
console.error('Tauri update check failed:', e);
});
});
useEffect(() => {
checkTauriUpdate();
}, []);
useInterval(
checkTauriUpdate,
(IS_TAURI && !DEBUG) ? TAURI_CHECK_UPDATE_INTERVAL : undefined,
);
const lang = useOldLang();
return (
@ -220,6 +246,7 @@ const LeftMain: FC<OwnProps> = ({
badge
className={buildClassName('btn-update', updateButtonClassNames)}
onClick={handleUpdateClick}
isLoading={isTauriUpdateDownloading}
>
{lang('lng_update_telegram')}
</Button>

View File

@ -108,7 +108,7 @@
position: relative;
margin-left: 0.8125rem;
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
body.is-tauri.is-macos #Main:not(.is-fullscreen) & {
margin-left: 0.5rem;
}
}

View File

@ -1,6 +1,6 @@
import type { FC } from '../../../lib/teact/teact';
import {
memo, useEffect, useMemo, useRef,
memo, useEffect, useMemo,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -21,14 +21,14 @@ import {
selectTheme,
} from '../../../global/selectors';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { IS_APP, IS_ELECTRON, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { formatDateToString } from '../../../util/dates/dateFormat';
import useAppLayout from '../../../hooks/useAppLayout';
import useConnectionStatus from '../../../hooks/useConnectionStatus';
import useElectronDrag from '../../../hooks/useElectronDrag';
import useFlag from '../../../hooks/useFlag';
import { useHotkeys } from '../../../hooks/useHotkeys';
import useLang from '../../../hooks/useLang';
@ -225,11 +225,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
handleDropdownMenuTransitionEnd,
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
const headerRef = useRef<HTMLDivElement>();
useElectronDrag(headerRef);
const withStoryToggler = !isSearchFocused
&& !selectedSearchDate && !globalSearchChatId && !areContactsVisible;
const withStoryToggler = !isSearchFocused && !selectedSearchDate && !globalSearchChatId && !areContactsVisible;
const searchContent = useMemo(() => {
return (
@ -260,13 +256,28 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
);
}, [globalSearchChatId, selectedSearchDate]);
const version = useMemo(() => {
let version = '';
if (IS_TAURI && window.tauri.version) {
version = `Tauri ${window.tauri.version} | `;
}
version += `${APP_NAME} ${versionString}`;
return version;
}, [versionString]);
return (
<div className="LeftMainHeader">
<div id="LeftMainHeader" className="left-header" ref={headerRef}>
<div
id="LeftMainHeader"
className="left-header"
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
>
{oldLang.isRtl && <div className="DropdownMenuFiller" />}
<DropdownMenu
trigger={MainButton}
footer={`${APP_NAME} ${versionString}`}
footer={version}
className={buildClassName(
'main-menu',
oldLang.isRtl && 'rtl',
@ -275,7 +286,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
)}
forceOpen={isBotMenuOpen}
positionX={shouldHideSearch && oldLang.isRtl ? 'right' : 'left'}
transformOriginX={IS_ELECTRON && IS_MAC_OS && !isFullscreen ? 90 : undefined}
transformOriginX={IS_TAURI && IS_MAC_OS && !isFullscreen ? 90 : undefined}
onTransitionEnd={oldLang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
>
<LeftSideMenuItems

View File

@ -26,7 +26,7 @@ import { selectTabState, selectTheme, selectUser } from '../../../global/selecto
import { selectPremiumLimit } from '../../../global/selectors/limits';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { IS_MULTIACCOUNT_SUPPORTED } from '../../../util/browser/globalEnvironment';
import { IS_ELECTRON } from '../../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { getPromptInstall } from '../../../util/installPrompt';
import { switchPermanentWebVersion } from '../../../util/permanentWebVersion';
@ -86,7 +86,7 @@ const LeftSideMenuItems = ({
const animationLevelValue = animationLevel !== ANIMATION_LEVEL_MIN
? (animationLevel === ANIMATION_LEVEL_MAX ? 'max' : 'mid') : 'min';
const withOtherVersions = !IS_ELECTRON && (window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST);
const withOtherVersions = !IS_TAURI && (window.location.hostname === PRODUCTION_HOSTNAME || IS_TEST);
const archivedUnreadChatsCount = useFolderManagerForUnreadCounters()[ARCHIVED_FOLDER_ID]?.chatsCount || 0;
@ -124,7 +124,7 @@ const LeftSideMenuItems = ({
});
const handleChangelogClick = useLastCallback(() => {
window.open(BETA_CHANGELOG_URL, '_blank', 'noopener');
window.open(BETA_CHANGELOG_URL, '_blank', 'noopener,noreferrer');
});
const handleSwitchToWebK = useLastCallback(() => {

View File

@ -1,23 +1,22 @@
import type { FC } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import {
memo, useCallback, useEffect, useRef, useState,
memo, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import { DEBUG_LOG_FILENAME } from '../../../config';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import {
IS_ELECTRON,
IS_SNAP_EFFECT_SUPPORTED,
IS_WAVE_TRANSFORM_SUPPORTED,
} from '../../../util/browser/windowEnvironment';
import { getDebugLogs } from '../../../util/debugConsole';
import download from '../../../util/download';
import { getAccountSlotUrl } from '../../../util/multiaccount';
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLastCallback from '../../../hooks/useLastCallback';
import useMultiaccountInfo from '../../../hooks/useMultiaccountInfo';
import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
@ -37,14 +36,14 @@ type StateProps = {
shouldDebugExportedSenders?: boolean;
};
const SettingsExperimental: FC<OwnProps & StateProps> = ({
const SettingsExperimental = ({
isActive,
onReset,
shouldForceHttpTransport,
shouldAllowHttpTransport,
shouldCollectDebugLogs,
shouldDebugExportedSenders,
}) => {
onReset,
}: OwnProps & StateProps) => {
const { requestConfetti, setSharedSettingOption, requestWave } = getActions();
const snapButtonRef = useRef<HTMLDivElement>();
@ -52,10 +51,7 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
const lang = useOldLang();
const [isAutoUpdateEnabled, setIsAutoUpdateEnabled] = useState(false);
useEffect(() => {
window.electron?.getIsAutoUpdateEnabled().then(setIsAutoUpdateEnabled);
}, []);
const accounts = useMultiaccountInfo();
useHistoryBack({
isActive,
@ -68,10 +64,6 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
download(url, DEBUG_LOG_FILENAME);
});
const handleIsAutoUpdateEnabledChange = useCallback((isChecked: boolean) => {
window.electron?.setIsAutoUpdateEnabled(isChecked);
}, []);
const handleRequestWave = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
requestWave({ startX: e.clientX, startY: e.clientY });
});
@ -93,6 +85,19 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
}
});
const newAccountUrl = useMemo(() => {
if (!Object.values(accounts).length) {
return undefined;
}
let freeIndex = 1;
while (accounts[freeIndex]) {
freeIndex += 1;
}
return getAccountSlotUrl(freeIndex, true, true);
}, [accounts]);
return (
<div className="settings-content custom-scroll">
<div className="settings-content-header no-border">
@ -105,6 +110,14 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
/>
<p className="settings-item-description pt-3" dir="auto">{lang('lng_settings_experimental_about')}</p>
</div>
<div className="settings-item">
<ListItem
href={newAccountUrl}
icon="add-user"
>
<div className="title">Login on Test Server</div>
</ListItem>
</div>
<div className="settings-item">
<ListItem
onClick={handleRequestConfetti}
@ -128,7 +141,8 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
>
<div className="title">Vaporize this button</div>
</ListItem>
</div>
<div className="settings-item">
<Checkbox
label="Allow HTTP Transport"
checked={Boolean(shouldAllowHttpTransport)}
@ -143,7 +157,8 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
onCheck={() => setSharedSettingOption({ shouldForceHttpTransport: !shouldForceHttpTransport })}
/>
</div>
<div className="settings-item">
<Checkbox
label={lang('DebugMenuEnableLogs')}
checked={Boolean(shouldCollectDebugLogs)}
@ -158,14 +173,6 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
onCheck={() => setSharedSettingOption({ shouldDebugExportedSenders: !shouldDebugExportedSenders })}
/>
{IS_ELECTRON && (
<Checkbox
label="Enable autoupdates"
checked={Boolean(isAutoUpdateEnabled)}
onCheck={handleIsAutoUpdateEnabledChange}
/>
)}
<ListItem
onClick={handleDownloadLog}
icon="bug"

View File

@ -1,6 +1,6 @@
import type { FC } from '../../../lib/teact/teact';
import {
memo, useCallback, useEffect, useState,
memo, useCallback,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -11,7 +11,7 @@ import { SettingsScreens } from '../../../types';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import {
IS_ANDROID, IS_ELECTRON, IS_IOS, IS_MAC_OS, IS_WINDOWS,
IS_ANDROID, IS_IOS, IS_MAC_OS,
} from '../../../util/browser/windowEnvironment';
import { setTimeFormat } from '../../../util/oldLangProvider';
import { getSystemTheme } from '../../../util/systemTheme';
@ -20,7 +20,6 @@ import useAppLayout from '../../../hooks/useAppLayout';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import RangeSlider from '../../ui/RangeSlider';
@ -114,15 +113,6 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
setSharedSettingOption({ messageSendKeyCombo: newCombo as SharedSettings['messageSendKeyCombo'] });
}, []);
const [isTrayIconEnabled, setIsTrayIconEnabled] = useState(false);
useEffect(() => {
window.electron?.getIsTrayIconEnabled().then(setIsTrayIconEnabled);
}, []);
const handleIsTrayIconEnabledChange = useCallback((isChecked: boolean) => {
window.electron?.setIsTrayIconEnabled(isChecked);
}, []);
useHistoryBack({
isActive,
onBack: onReset,
@ -149,14 +139,6 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
>
{lang('ChatBackground')}
</ListItem>
{IS_ELECTRON && IS_WINDOWS && (
<Checkbox
label={lang('SettingsTray')}
checked={Boolean(isTrayIconEnabled)}
onCheck={handleIsTrayIconEnabledChange}
/>
)}
</div>
<div className="settings-item">

View File

@ -10,7 +10,6 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatFolder, ApiLimitTypeWithModal, ApiUser } from '../../api/types';
import type { TabState } from '../../global/types';
import { ElectronEvent } from '../../types/electron';
import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER } from '../../config';
import { requestNextMutation } from '../../lib/fasterdom/fasterdom';
@ -32,7 +31,8 @@ import {
selectUser,
} from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState';
import { IS_ANDROID, IS_ELECTRON, IS_WAVE_TRANSFORM_SUPPORTED } from '../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_ANDROID, IS_WAVE_TRANSFORM_SUPPORTED } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink';
@ -42,6 +42,7 @@ import updateIcon from '../../util/updateIcon';
import useInterval from '../../hooks/schedulers/useInterval';
import useTimeout from '../../hooks/schedulers/useTimeout';
import useTauriEvent from '../../hooks/tauri/useTauriEvent';
import useAppLayout from '../../hooks/useAppLayout';
import useForceUpdate from '../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang';
@ -242,7 +243,6 @@ const Main = ({
loadRecentReactions,
loadDefaultTagReactions,
loadFeaturedEmojiStickers,
setIsElectronUpdateAvailable,
loadAuthorizations,
loadPeerColors,
loadSavedReactionTags,
@ -289,26 +289,6 @@ const Main = ({
useInterval(checkAppVersion, isMasterTab ? APP_OUTDATED_TIMEOUT_MS : undefined, true);
useEffect(() => {
if (!IS_ELECTRON) {
return undefined;
}
const removeUpdateAvailableListener = window.electron!.on(ElectronEvent.UPDATE_AVAILABLE, () => {
setIsElectronUpdateAvailable({ isAvailable: true });
});
const removeUpdateErrorListener = window.electron!.on(ElectronEvent.UPDATE_ERROR, () => {
setIsElectronUpdateAvailable({ isAvailable: false });
removeUpdateAvailableListener?.();
});
return () => {
removeUpdateErrorListener?.();
removeUpdateAvailableListener?.();
};
}, []);
// Initial API calls
useEffect(() => {
if (isMasterTab && isSynced) {
@ -432,11 +412,18 @@ const Main = ({
}
}, [isSynced]);
useEffect(() => {
return window.electron?.on(ElectronEvent.DEEPLINK, (link: string) => {
processDeepLink(decodeURIComponent(link));
});
}, []);
useTauriEvent<string>('deeplink', (event) => {
try {
const url = event.payload || '';
const decodedUrl = decodeURIComponent(url);
processDeepLink(decodedUrl);
} catch (e) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error('Failed to process deep link', e);
}
}
});
useEffect(() => {
const parsedLocationHash = parseLocationHash(currentUserId);
@ -554,7 +541,7 @@ const Main = ({
});
// Online status and browser tab indicators
useBackgroundMode(handleBlur, handleFocus, Boolean(IS_ELECTRON));
useBackgroundMode(handleBlur, handleFocus, IS_TAURI);
useBeforeUnload(handleBlur);
usePreventPinchZoomGesture(isMediaViewerOpen || isStoryViewerOpen);

View File

@ -84,12 +84,8 @@
min-width: 0;
}
body.is-electron.is-macos & {
-webkit-app-region: drag;
}
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
padding-left: 5rem;
body.is-tauri.is-macos #Main:not(.is-fullscreen) & {
padding-left: var(--window-controls-width);
}
@media (max-width: 600px) {

View File

@ -36,6 +36,8 @@ import {
selectTabState,
} from '../../global/selectors';
import { stopCurrentAudio } from '../../util/audioPlayer';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { isUserId } from '../../util/entities/ids';
@ -46,7 +48,6 @@ import selectViewableMedia from './helpers/getViewableMedia';
import { animateClosing, animateOpening } from './helpers/ghostAnimation';
import useAppLayout from '../../hooks/useAppLayout';
import useElectronDrag from '../../hooks/useElectronDrag';
import useFlag from '../../hooks/useFlag';
import useForceUpdate from '../../hooks/useForceUpdate';
import useLastCallback from '../../hooks/useLastCallback';
@ -209,9 +210,6 @@ const MediaViewer = ({
}
}, [isMobile, isOpen]);
const headerRef = useRef<HTMLDivElement>();
useElectronDrag(headerRef);
const forceUpdate = useForceUpdate();
useEffect(() => {
const mql = window.matchMedia(MEDIA_VIEWER_MEDIA_QUERY);
@ -422,7 +420,11 @@ const MediaViewer = ({
shouldAnimateFirstRender
noCloseTransition={shouldSkipHistoryAnimations}
>
<div className="media-viewer-head" dir={lang.isRtl ? 'rtl' : undefined} ref={headerRef}>
<div
className="media-viewer-head"
dir={lang.isRtl ? 'rtl' : undefined}
data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}
>
{isMobile && (
<Button
className="media-viewer-close"

View File

@ -9,7 +9,7 @@
height: 1rem;
:global(body.is-electron) & {
:global(body.is-tauri) & {
cursor: auto;
}
}

View File

@ -45,10 +45,6 @@
}
}
&.draggable {
-webkit-app-region: drag;
}
&.customBgImage.blurred::before {
filter: blur(12px);
}

View File

@ -55,13 +55,9 @@ import {
selectUserFullInfo,
} from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState.ts';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import {
IS_ANDROID,
IS_ELECTRON,
IS_IOS,
IS_SAFARI,
IS_TRANSLATION_SUPPORTED,
MASK_IMAGE_DISABLED,
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
} from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
@ -449,7 +445,6 @@ function MiddleColumn({
backgroundColor && styles.customBgColor,
customBackground && isBackgroundBlurred && styles.blurred,
isRightColumnShown && styles.withRightColumn,
IS_ELECTRON && !(renderingChatId && renderingThreadId) && styles.draggable,
);
const messagingDisabledClassName = buildClassName(
@ -532,6 +527,7 @@ function MiddleColumn({
<div
className={bgClassName}
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
data-tauri-drag-region={IS_TAURI && IS_MAC_OS && !(renderingChatId && renderingThreadId) ? true : undefined}
/>
<div id="middle-column-portals" />
{Boolean(renderingChatId && renderingThreadId) && (

View File

@ -325,13 +325,9 @@
}
}
body.is-electron.is-macos & {
-webkit-app-region: drag;
}
body.is-electron.is-macos #Main:not(.left-column-open):not(.is-fullscreen) & {
body.is-tauri.is-macos #Main:not(.left-column-open):not(.is-fullscreen) & {
@media (max-width: 925px) {
padding-left: 5rem;
padding-left: var(--window-controls-width);
.back-button {
margin-left: -0.5rem;

View File

@ -34,12 +34,13 @@ import {
selectThreadInfo,
selectThreadParam,
} from '../../global/selectors';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids';
import useAppLayout from '../../hooks/useAppLayout';
import useConnectionStatus from '../../hooks/useConnectionStatus';
import useElectronDrag from '../../hooks/useElectronDrag';
import useLastCallback from '../../hooks/useLastCallback';
import useLongPress from '../../hooks/useLongPress';
import useOldLang from '../../hooks/useOldLang';
@ -130,12 +131,10 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const lang = useOldLang();
const isBackButtonActive = useRef(true);
const { isTablet } = useAppLayout();
const { isDesktop, isTablet } = useAppLayout();
const { width: windowWidth } = useWindowSize();
const { isDesktop } = useAppLayout();
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
const shouldShowCloseButton = isTablet && isLeftColumnShown;
@ -334,10 +333,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
);
}
useElectronDrag(componentRef);
return (
<div className="MiddleHeader" ref={componentRef}>
<div className="MiddleHeader" ref={componentRef} data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : 'slideFade'}
activeKey={currentTransitionKey}

View File

@ -0,0 +1,92 @@
#MobileSearch > .header {
position: absolute;
z-index: var(--z-mobile-search);
top: 0;
left: 0;
display: flex;
align-items: center;
width: 100%;
height: 3.5rem;
padding-right: max(0.5rem, env(safe-area-inset-right));
padding-left: max(0.25rem, env(safe-area-inset-left));
background: var(--color-background);
> .SearchInput {
flex: 1;
margin-left: 0.25rem;
}
body.is-tauri.is-macos & {
padding-left: var(--window-controls-width);
}
}
#MobileSearch > .tags-subheader {
--color-reaction: var(--color-background-secondary);
--hover-color-reaction: var(--color-background-secondary-accent);
--text-color-reaction: var(--color-text-secondary);
--color-reaction-chosen: var(--color-primary);
--text-color-reaction-chosen: #FFFFFF;
--hover-color-reaction-chosen: var(--color-primary-shade);
position: absolute;
z-index: var(--z-mobile-search);
top: 3.5rem;
left: 0;
overflow-x: scroll;
display: flex;
gap: 0.375rem;
align-items: center;
width: 100%;
height: 3rem;
padding-right: max(0.5rem, env(safe-area-inset-right));
padding-left: max(0.25rem, env(safe-area-inset-left));
background: var(--color-background);
}
#MobileSearch > .footer {
position: absolute;
z-index: var(--z-mobile-search);
bottom: 0;
left: 0;
display: flex;
align-items: center;
width: 100%;
height: 3.5rem;
padding-right: max(0.5rem, env(safe-area-inset-right));
padding-left: max(1rem, env(safe-area-inset-left));
background: var(--color-background);
body:not(.keyboard-visible) & {
height: 3.5rem;
padding-bottom: 0;
}
> .counter {
flex: 1;
color: var(--color-text-secondary);
}
@media (max-width: 600px) {
body:not(.keyboard-visible) & {
height: calc(3.5rem + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
}
}
}
#MobileSearch:not(.active) {
.header, .tags-subheader, .footer {
// `display: none` will prevent synchronous focus on iOS
transform: translateX(-999rem);
}
}

View File

@ -2,7 +2,6 @@ import type { FC } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import { memo } from '../../../lib/teact/teact';
import { BASE_URL, IS_PACKAGED_ELECTRON } from '../../../config';
import { IS_EMOJI_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { handleEmojiLoad, LOADED_EMOJIS } from '../../../util/emoji/emoji';
@ -32,7 +31,7 @@ const EmojiButton: FC<OwnProps> = ({
focus && 'focus',
);
const src = `${IS_PACKAGED_ELECTRON ? BASE_URL : '.'}/img-apple-64/${emoji.image}.png`;
const src = `./img-apple-64/${emoji.image}.png`;
const isLoaded = LOADED_EMOJIS.has(src);
return (

View File

@ -19,6 +19,7 @@ import { EDITABLE_INPUT_ID } from '../../../config';
import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fasterdom';
import { selectCanPlayAnimatedEmojis, selectDraft, selectIsInSelectMode } from '../../../global/selectors';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import {
IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_TOUCH_ENV,
} from '../../../util/browser/windowEnvironment';
@ -581,6 +582,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
contentEditable={isAttachmentModalInput || canSendPlainText}
role="textbox"
dir="auto"
spellCheck={IS_TAURI ? false : undefined}
tabIndex={0}
onClick={focusInput}
onChange={handleChange}

View File

@ -8,6 +8,7 @@ import type { IAnchorPosition } from '../../../types';
import { ApiMessageEntityTypes } from '../../../api/types';
import { EDITABLE_INPUT_ID } from '../../../config';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { ensureProtocol } from '../../../util/browser/url';
import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
@ -488,6 +489,7 @@ const TextFormatter: FC<OwnProps> = ({
value={linkUrl}
placeholder={lang('FormattingEnterUrl')}
autoComplete="off"
spellCheck={IS_TAURI ? false : undefined}
inputMode="url"
dir="auto"
onChange={handleLinkUrlChange}

View File

@ -27,7 +27,8 @@ import {
selectTabState,
selectTheme,
} from '../../../global/selectors';
import { IS_ANDROID, IS_ELECTRON, IS_FLUID_BACKGROUND_SUPPORTED } from '../../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_ANDROID, IS_FLUID_BACKGROUND_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { isLocalMessageId } from '../../../util/keys/messageKey';
import { isElementInViewport } from '../../../util/visibility/isElementInViewport';
@ -199,7 +200,7 @@ const ActionMessage = ({
} = useContextMenuHandlers(
ref,
(isTouchScreen && isInSelectMode) || isAccountFrozen,
!IS_ELECTRON,
!IS_TAURI,
IS_ANDROID,
getIsMessageListReady,
);

View File

@ -124,7 +124,8 @@ import {
selectMessageTimestampableDuration,
} from '../../../global/selectors/media';
import { selectSharedSettings } from '../../../global/selectors/sharedState';
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { isUserId } from '../../../util/entities/ids';
import { getMessageKey } from '../../../util/keys/messageKey';
@ -496,7 +497,7 @@ const Message: FC<OwnProps & StateProps> = ({
} = useContextMenuHandlers(
ref,
(isTouchScreen && isInSelectMode) || isAccountFrozen,
!IS_ELECTRON,
!IS_TAURI,
IS_ANDROID,
getIsMessageListReady,
);

View File

@ -57,8 +57,6 @@
position: relative;
unicode-bidi: plaintext;
overflow: clip;
overflow-clip-margin: 0.5rem;
display: block;
margin: 0;
@ -68,6 +66,11 @@
text-align: initial;
overflow-wrap: anywhere;
white-space: pre-wrap;
@supports (overflow-clip-margin: 0.5rem) {
overflow: clip;
overflow-clip-margin: 0.5rem;
}
}
.transcription {

View File

@ -48,8 +48,8 @@
opacity: 1;
}
:global(body.is-electron.is-macos) & {
padding-left: 4.5rem;
:global(body.is-tauri.is-macos) & {
padding-left: var(--window-controls-width);
}
@media (max-width: 600px) {

View File

@ -59,17 +59,9 @@
margin-left: auto;
}
body.is-electron.is-macos & {
-webkit-app-region: drag;
.SearchInput {
-webkit-app-region: no-drag;
}
}
body.is-electron.is-macos #Main:not(.is-fullscreen) & {
body.is-tauri.is-macos #Main:not(.is-fullscreen) & {
@media (max-width: 600px) {
padding-left: 5rem;
padding-left: var(--window-controls-width);
}
}

View File

@ -1,6 +1,6 @@
import type { FC } from '../../lib/teact/teact';
import {
useEffect, useMemo, useRef, useState,
useEffect, useMemo, useState,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
@ -26,12 +26,13 @@ import {
selectTopic,
selectUser,
} from '../../global/selectors';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids';
import useAppLayout from '../../hooks/useAppLayout';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useElectronDrag from '../../hooks/useElectronDrag';
import useFlag from '../../hooks/useFlag';
import { useFolderManagerForChatsCount } from '../../hooks/useFolderManager';
import useLang from '../../hooks/useLang';
@ -695,11 +696,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
(shouldSkipTransition || shouldSkipHistoryAnimations) && 'no-transition',
);
const headerRef = useRef<HTMLDivElement>();
useElectronDrag(headerRef);
return (
<div className="RightHeader" ref={headerRef}>
<div className="RightHeader" data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}>
<Button
className="close-button"
round

View File

@ -2,9 +2,9 @@ import type {
ChangeEvent, FormEvent,
} from 'react';
import type { ElementRef, FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import buildClassName from '../../util/buildClassName';
import useOldLang from '../../hooks/useOldLang';
@ -81,6 +81,7 @@ const InputText: FC<OwnProps> = ({
placeholder={placeholder}
maxLength={maxLength}
autoComplete={autoComplete}
spellCheck={IS_TAURI ? false : undefined}
inputMode={inputMode}
disabled={disabled}
readOnly={readOnly}

View File

@ -1,4 +1,4 @@
import type { ElementRef, FC, TeactNode } from '../../lib/teact/teact';
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import { useRef } from '../../lib/teact/teact';
@ -78,7 +78,7 @@ interface OwnProps {
nonInteractive?: boolean;
}
const ListItem: FC<OwnProps> = ({
const ListItem = ({
ref,
buttonRef,
icon,
@ -114,7 +114,7 @@ const ListItem: FC<OwnProps> = ({
onSecondaryIconClick,
onDragEnter,
nonInteractive,
}) => {
}: OwnProps) => {
let containerRef = useRef<HTMLDivElement>();
if (ref) {
containerRef = ref;
@ -138,12 +138,13 @@ const ListItem: FC<OwnProps> = ({
const handleClickEvent = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
const hasModifierKey = e.ctrlKey || e.metaKey || e.shiftKey;
if (!hasModifierKey && e.button === MouseButton.Main) {
if (href && !onClick) return; // Allow default behavior for opening links
e.preventDefault();
}
});
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if ((disabled && !allowDisabledClick) || !onClick) {
if ((disabled && !allowDisabledClick)) {
return;
}
@ -154,9 +155,11 @@ const ListItem: FC<OwnProps> = ({
return;
}
e.preventDefault();
if (onClick) e.preventDefault();
}
if (!onClick) return;
onClick(e, clickArg);
if (IS_TOUCH_ENV && !ripple) {
@ -229,13 +232,16 @@ const ListItem: FC<OwnProps> = ({
>
<ButtonElementTag
className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)}
role={!isStatic ? 'button' : undefined}
role={!isStatic && !href ? 'button' : undefined}
href={href}
ref={buttonRef as any /* TS requires specific types for refs */}
// @ts-expect-error TS requires specific types for refs
ref={buttonRef}
rel={href ? 'noopener noreferrer' : undefined}
tabIndex={!isStatic ? 0 : undefined}
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : handleClickEvent}
onMouseDown={handleMouseDown}
onContextMenu={onContextMenu || ((!inactive && contextActions) ? handleContextMenu : undefined)}
aria-disabled={disabled || undefined}
>
{!disabled && !inactive && ripple && (
<RippleEffect />

View File

@ -1,10 +1,10 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { ElementRef, FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import {
memo, useEffect, useRef,
} from '../../lib/teact/teact';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import buildClassName from '../../util/buildClassName';
import useFlag from '../../hooks/useFlag';
@ -186,6 +186,7 @@ const SearchInput: FC<OwnProps> = ({
value={value}
disabled={disabled}
autoComplete={autoComplete}
spellCheck={IS_TAURI ? false : undefined}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}

View File

@ -6,6 +6,7 @@ import {
} from '../../lib/teact/teact';
import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import buildClassName from '../../util/buildClassName';
import useLastCallback from '../../hooks/useLastCallback';
@ -119,6 +120,7 @@ const TextArea: FC<OwnProps> = ({
placeholder={placeholder}
maxLength={maxLength}
autoComplete={autoComplete}
spellCheck={IS_TAURI ? false : undefined}
inputMode={inputMode}
disabled={disabled}
readOnly={readOnly}

View File

@ -49,6 +49,7 @@ export type TransitionProps = {
children: React.ReactNode | ChildrenFn;
contentSelector?: string;
restoreHeightKey?: number;
'data-tauri-drag-region'?: true;
};
const FALLBACK_ANIMATION_END = 1000;
@ -93,6 +94,7 @@ function Transition({
children,
contentSelector,
restoreHeightKey,
'data-tauri-drag-region': dataTauriDragRegion,
}: TransitionProps) {
const currentKeyRef = useRef<number>();
// No need for a container to update on change
@ -406,6 +408,7 @@ function Transition({
id={id}
className={buildClassName('Transition', className)}
teactFastList={asFastList}
data-tauri-drag-region={dataTauriDragRegion}
onScroll={onScroll}
onMouseDown={onMouseDown}
>

View File

@ -20,10 +20,7 @@ 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_PACKAGED_ELECTRON = process.env.IS_PACKAGED_ELECTRON;
export const ELECTRON_WINDOW_DRAG_EVENT_START = 'tt-electron-window-drag-start';
export const ELECTRON_WINDOW_DRAG_EVENT_END = 'tt-electron-window-drag-end';
export const PAID_MESSAGES_PURPOSE = 'paid_messages';
export const DEBUG = process.env.APP_ENV !== 'production';
@ -33,7 +30,6 @@ export const STRICTERDOM_ENABLED = DEBUG;
export const BOT_VERIFICATION_PEERS_LIMIT = 20;
export const BETA_CHANGELOG_URL = 'https://telegra.ph/WebA-Beta-03-20';
export const ELECTRON_HOST_URL = process.env.ELECTRON_HOST_URL!;
export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.';
export const DEBUG_GRAMJS = false;

View File

@ -1,104 +0,0 @@
import {
app, BrowserWindow, ipcMain, net,
} from 'electron';
import type { UpdateInfo } from 'electron-updater';
import { autoUpdater } from 'electron-updater';
import type { WindowState } from './windowState';
import { ElectronAction, ElectronEvent } from '../types/electron';
import { PRODUCTION_URL } from '../config';
import getIsAppUpdateNeeded from '../util/getIsAppUpdateNeeded';
import { pause } from '../util/schedulers';
import {
forceQuit, IS_MAC_OS, IS_PREVIEW, IS_WINDOWS, store,
} from './utils';
export const AUTO_UPDATE_SETTING_KEY = 'autoUpdate';
const ELECTRON_APP_VERSION_URL = 'electronVersion.txt';
const CHECK_UPDATE_INTERVAL = 5 * 60 * 1000;
let isUpdateCheckStarted = false;
export default function setupAutoUpdates(state: WindowState) {
if (isUpdateCheckStarted) {
return;
}
isUpdateCheckStarted = true;
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
checkForUpdates();
ipcMain.handle(ElectronAction.INSTALL_UPDATE, () => {
state.saveLastUrlHash();
if (IS_MAC_OS || IS_WINDOWS) {
forceQuit.enable();
}
return autoUpdater.quitAndInstall();
});
autoUpdater.on('error', (error: Error) => {
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send(ElectronEvent.UPDATE_ERROR, error);
});
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send(ElectronEvent.UPDATE_AVAILABLE, info);
});
});
}
export function getIsAutoUpdateEnabled() {
return !IS_PREVIEW && store.get(AUTO_UPDATE_SETTING_KEY);
}
async function checkForUpdates(): Promise<void> {
while (true) {
if (await shouldPerformAutoUpdate()) {
if (getIsAutoUpdateEnabled()) {
autoUpdater.checkForUpdates();
} else {
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send(ElectronEvent.UPDATE_AVAILABLE);
});
}
}
await pause(CHECK_UPDATE_INTERVAL);
}
}
function shouldPerformAutoUpdate(): Promise<boolean> {
return new Promise((resolve) => {
const request = net.request(`${PRODUCTION_URL}/${ELECTRON_APP_VERSION_URL}?${Date.now()}`);
request.on('response', (response) => {
let contents = '';
response.on('end', () => {
resolve(getIsAppUpdateNeeded(contents, app.getVersion(), true));
});
response.on('data', (data: Buffer) => {
contents = `${contents}${String(data)}`;
});
response.on('error', () => {
resolve(false);
});
});
request.on('error', () => {
resolve(false);
});
request.end();
});
}

View File

@ -1,87 +0,0 @@
/* eslint-disable no-template-curly-in-string */
const config = {
productName: 'Telegram A',
artifactName: '${productName}-${arch}.${ext}',
appId: 'org.telegram.TelegramA',
extraMetadata: {
main: './dist/electron.cjs',
productName: 'Telegram A',
},
asarUnpack: [
'build/Release/electron_drag_click.node',
],
files: [
'dist',
'package.json',
'public/icon-electron-windows.ico',
'build/Release/electron_drag_click.node',
'!dist/**/build-stats.json',
'!dist/**/statoscope-report.html',
'!dist/**/reference.json',
'!dist/img-apple-*',
'!dist/get',
'!node_modules',
],
directories: {
buildResources: './public',
output: './dist-electron',
},
protocols: [
{
name: 'Tg',
schemes: ['tg'],
},
],
publish: {
provider: 'github',
owner: 'Ajaxy',
repo: 'telegram-tt',
releaseType: 'draft',
},
win: {
target: {
target: 'nsis',
arch: ['x64'],
},
icon: 'public/icon-electron-windows.ico',
},
nsis: {
oneClick: false,
createDesktopShortcut: true,
createStartMenuShortcut: true,
},
mac: {
target: {
target: 'default',
arch: ['x64', 'arm64'],
},
entitlements: 'public/electron-entitlements.mac.plist',
icon: 'public/icon-electron-macos.icns',
},
dmg: {
background: 'public/background-electron-dmg.tiff',
iconSize: 100,
contents: [
{
x: 138,
y: 225,
type: 'file',
},
{
x: 402,
y: 225,
type: 'link',
path: '/Applications',
},
],
},
linux: {
category: 'Chat;Network;InstantMessaging;',
target: {
target: 'AppImage',
arch: ['x64'],
},
},
};
export default config;

View File

@ -1,70 +0,0 @@
import { app } from 'electron';
import path from 'path';
import { ElectronEvent } from '../types/electron';
import {
focusLastWindow, getLastWindow, IS_LINUX, IS_MAC_OS, IS_WINDOWS,
} from './utils';
const TG_PROTOCOL = 'tg';
let deeplinkUrl: string | undefined;
export function initDeeplink() {
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(TG_PROTOCOL, process.execPath, [path.resolve(process.argv[1])]);
}
} else {
app.setAsDefaultProtocolClient(TG_PROTOCOL);
}
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
app.on('will-finish-launching', () => {
app.on('open-url', (event: Electron.Event, url: string) => {
event.preventDefault();
deeplinkUrl = url;
processDeeplink();
focusLastWindow();
});
});
if (IS_WINDOWS || IS_LINUX) {
deeplinkUrl = findDeeplink(process.argv);
}
app.on('second-instance', (_, argv: string[]) => {
if (IS_MAC_OS) {
deeplinkUrl = argv[0];
} else {
deeplinkUrl = findDeeplink(argv);
}
processDeeplink();
focusLastWindow();
});
}
export function processDeeplink() {
const window = getLastWindow();
if (!window || !deeplinkUrl) {
return;
}
window.webContents.send(ElectronEvent.DEEPLINK, deeplinkUrl);
deeplinkUrl = undefined;
}
function findDeeplink(args: string[]) {
return args.find((arg) => arg.startsWith(`${TG_PROTOCOL}://`));
}

View File

@ -1,43 +0,0 @@
import { checkIsWebContentsUrlAllowed, getLastWindow } from './utils';
let localStorage: Record<string, any> | undefined;
export async function captureLocalStorage(): Promise<void> {
const lastWindow = getLastWindow();
if (!lastWindow) {
return;
}
const contents = lastWindow.webContents;
const contentsUrl = contents.getURL();
if (!checkIsWebContentsUrlAllowed(contentsUrl)) {
return;
}
localStorage = await contents.executeJavaScript('({ ...localStorage });');
}
export async function restoreLocalStorage(): Promise<void> {
const lastWindow = getLastWindow();
if (!lastWindow || !localStorage) {
return;
}
const contents = lastWindow.webContents;
const contentsUrl = contents.getURL();
if (!checkIsWebContentsUrlAllowed(contentsUrl)) {
return;
}
await contents.executeJavaScript(
Object.keys(localStorage).map(
(key: string) => `localStorage.setItem('${key}', JSON.stringify(${localStorage![key]}))`,
).join(';'),
);
localStorage = undefined;
}

View File

@ -1,36 +0,0 @@
import { app, nativeImage } from 'electron';
import contextMenu from 'electron-context-menu';
import electronDragClick from 'electron-drag-click';
import path from 'path';
import { initDeeplink } from './deeplink';
import { IS_MAC_OS, IS_PRODUCTION, IS_WINDOWS } from './utils';
import { createWindow, setupCloseHandlers, setupElectronActionHandlers } from './window';
initDeeplink();
if (IS_MAC_OS) {
electronDragClick();
}
contextMenu({
showLearnSpelling: false,
showLookUpSelection: false,
showSearchWithGoogle: false,
showCopyImage: false,
showSelectAll: true,
showInspectElement: !IS_PRODUCTION,
});
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();
});

View File

@ -1,37 +0,0 @@
import type { IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';
import type { ElectronApi, ElectronEvent, WindowButtonsPosition } 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),
setWindowTitle: (title?: string) => ipcRenderer.invoke(ElectronAction.SET_WINDOW_TITLE, title),
setWindowButtonsPosition:
(position: WindowButtonsPosition) => ipcRenderer.invoke(ElectronAction.SET_WINDOW_BUTTONS_POSITION, position),
/**
* @deprecated Use `setWindowButtonsPosition` instead
*/
setTrafficLightPosition:
(position: WindowButtonsPosition) => ipcRenderer.invoke(ElectronAction.SET_WINDOW_BUTTONS_POSITION, position),
setIsAutoUpdateEnabled: (value: boolean) => ipcRenderer.invoke(ElectronAction.SET_IS_AUTO_UPDATE_ENABLED, value),
getIsAutoUpdateEnabled: () => ipcRenderer.invoke(ElectronAction.GET_IS_AUTO_UPDATE_ENABLED),
setIsTrayIconEnabled: (value: boolean) => ipcRenderer.invoke(ElectronAction.SET_IS_TRAY_ICON_ENABLED, value),
getIsTrayIconEnabled: () => ipcRenderer.invoke(ElectronAction.GET_IS_TRAY_ICON_ENABLED),
restoreLocalStorage: () => ipcRenderer.invoke(ElectronAction.RESTORE_LOCAL_STORAGE),
on: (eventName: ElectronEvent, callback) => {
const subscription = (event: IpcRendererEvent, ...args: unknown[]) => callback(...args);
ipcRenderer.on(eventName, subscription);
return () => {
ipcRenderer.removeListener(eventName, subscription);
};
},
};
contextBridge.exposeInMainWorld('electron', electronApi);

View File

@ -1,99 +0,0 @@
import {
app, BrowserWindow, Menu, nativeImage, Tray,
} from 'electron';
import path from 'path';
import {
focusLastWindow, forceQuit, getAppTitle, store,
} from './utils';
const TRAY_ICON_SETTINGS_KEY = 'trayIcon';
const WINDOW_BLUR_TIMEOUT = 800;
interface TrayHelper {
instance?: Tray;
lastFocusedWindow?: BrowserWindow;
lastFocusedWindowTimer?: ReturnType<typeof setTimeout>;
setupListeners: (window: BrowserWindow) => void;
create: () => void;
enable: () => void;
disable: () => void;
isEnabled: boolean;
}
const tray: TrayHelper = {
setupListeners(window: BrowserWindow) {
window.on('focus', () => {
clearTimeout(this.lastFocusedWindowTimer);
this.lastFocusedWindow = window;
});
window.on('blur', () => {
this.lastFocusedWindowTimer = setTimeout(() => {
if (this.lastFocusedWindow === window) {
this.lastFocusedWindow = undefined;
}
}, WINDOW_BLUR_TIMEOUT);
});
window.on('close', () => {
this.lastFocusedWindow = undefined;
});
},
create() {
if (this.instance) {
return;
}
const icon = nativeImage.createFromPath(path.resolve(__dirname, '../public/icon-electron-windows.ico'));
const title = getAppTitle();
this.instance = new Tray(icon);
const handleOpenFromTray = () => {
focusLastWindow();
};
const handleCloseFromTray = () => {
forceQuit.enable();
app.quit();
};
const handleTrayClick = () => {
if (this.lastFocusedWindow) {
BrowserWindow.getAllWindows().forEach((window) => window.hide());
this.lastFocusedWindow = undefined;
} else {
handleOpenFromTray();
}
};
const contextMenu = Menu.buildFromTemplate([
{ label: `Open ${title}`, click: handleOpenFromTray },
{ label: `Quit ${title}`, click: handleCloseFromTray },
]);
this.instance.on('click', handleTrayClick);
this.instance.setContextMenu(contextMenu);
this.instance.setToolTip(title);
this.instance.setTitle(title);
},
enable() {
store.set(TRAY_ICON_SETTINGS_KEY, true);
this.create();
},
disable() {
store.set(TRAY_ICON_SETTINGS_KEY, false);
this.instance?.destroy();
this.instance = undefined;
},
get isEnabled(): boolean {
return store.get(TRAY_ICON_SETTINGS_KEY, true) as boolean;
},
};
export default tray;

View File

@ -1,101 +0,0 @@
import type { Point } from 'electron';
import { app, BrowserWindow } from 'electron';
import { Conf } from 'electron-conf/main';
import fs from 'fs';
import type { WindowButtonsPosition } from '../types/electron';
import { BASE_URL, PRODUCTION_URL } from '../config';
const ALLOWED_URL_ORIGINS = [BASE_URL!, PRODUCTION_URL].map((url) => (new URL(url).origin));
export const IS_MAC_OS = process.platform === 'darwin';
export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux';
export const IS_PREVIEW = process.env.IS_PREVIEW === 'true';
export const IS_FIRST_RUN = !fs.existsSync(`${app.getPath('userData')}/config.json`);
export const IS_PRODUCTION = process.env.APP_ENV === 'production';
export const windows = new Set<BrowserWindow>();
export const store = new Conf();
export function getCurrentWindow(): BrowserWindow | null {
return BrowserWindow.getFocusedWindow();
}
export function getLastWindow(): BrowserWindow | undefined {
return Array.from(windows).pop();
}
export function hasExtraWindows(): boolean {
return BrowserWindow.getAllWindows().length > 1;
}
export function reloadWindows(isAutoUpdateEnabled = true): void {
BrowserWindow.getAllWindows().forEach((window: BrowserWindow) => {
const { hash } = new URL(window.webContents.getURL());
if (isAutoUpdateEnabled) {
window.loadURL(`${process.env.BASE_URL}${hash}`);
} else {
window.loadURL(`file://${__dirname}/index.html${hash}`);
}
});
}
export function focusLastWindow(): void {
if (BrowserWindow.getAllWindows().every((window) => !window.isVisible())) {
BrowserWindow.getAllWindows().forEach((window) => window.show());
} else {
getLastWindow()?.focus();
}
}
export function getAppTitle(chatTitle?: string): string {
const appName = app.getName();
if (!chatTitle) {
return appName;
}
return `${chatTitle} · ${appName}`;
}
export function checkIsWebContentsUrlAllowed(url: string): boolean {
if (!app.isPackaged) {
return true;
}
const parsedUrl = new URL(url);
const localContentsPathname = IS_WINDOWS
? encodeURI(`/${__dirname.replace(/\\/g, '/')}/index.html`)
: encodeURI(`${__dirname}/index.html`);
if (parsedUrl.pathname === localContentsPathname) {
return true;
}
return ALLOWED_URL_ORIGINS.includes(parsedUrl.origin);
}
export const WINDOW_BUTTONS_POSITION: Record<WindowButtonsPosition, Point> = {
standard: { x: 10, y: 20 },
lowered: { x: 10, y: 52 },
};
export const forceQuit = {
value: false,
enable() {
this.value = true;
},
disable() {
this.value = false;
},
get isEnabled(): boolean {
return this.value;
},
};

View File

@ -1,252 +0,0 @@
import type { HandlerDetails } from 'electron';
import {
app, BrowserWindow, ipcMain, shell, systemPreferences,
} from 'electron';
import path from 'path';
import type { WindowButtonsPosition } from '../types/electron';
import { ElectronAction, ElectronEvent } from '../types/electron';
import setupAutoUpdates, { AUTO_UPDATE_SETTING_KEY, getIsAutoUpdateEnabled } from './autoUpdates';
import { processDeeplink } from './deeplink';
import { captureLocalStorage, restoreLocalStorage } from './localStorage';
import tray from './tray';
import {
checkIsWebContentsUrlAllowed, forceQuit, getAppTitle, getCurrentWindow, getLastWindow,
hasExtraWindows, IS_FIRST_RUN, IS_MAC_OS, IS_PREVIEW, IS_PRODUCTION, IS_WINDOWS,
reloadWindows, store, WINDOW_BUTTONS_POSITION, windows,
} from './utils';
import windowStateKeeper from './windowState';
const ALLOWED_DEVICE_ORIGINS = ['http://localhost:1234', 'file://'];
export function createWindow(url?: string) {
const windowState = windowStateKeeper({
defaultWidth: 1088,
defaultHeight: 700,
});
let x;
let y;
const currentWindow = getCurrentWindow();
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: getAppTitle(),
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
devTools: !IS_PRODUCTION,
},
...(IS_MAC_OS && {
titleBarStyle: 'hidden',
trafficLightPosition: WINDOW_BUTTONS_POSITION.standard,
}),
});
windowState.manage(window);
window.webContents.setWindowOpenHandler((details: HandlerDetails) => {
shell.openExternal(details.url);
return { action: 'deny' };
});
window.webContents.session.setDevicePermissionHandler(({ deviceType, origin }) => {
return deviceType === 'hid' && ALLOWED_DEVICE_ORIGINS.includes(origin);
});
window.webContents.on('will-navigate', (event, newUrl) => {
if (!checkIsWebContentsUrlAllowed(newUrl)) {
event.preventDefault();
}
});
window.on('page-title-updated', (event: Electron.Event) => {
event.preventDefault();
});
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 || (IS_WINDOWS && tray.isEnabled)) {
if (forceQuit.isEnabled) {
app.exit(0);
forceQuit.disable();
} else if (hasExtraWindows()) {
windows.delete(window);
windowState.unmanage();
} else {
event.preventDefault();
window.hide();
}
}
});
windowState.clearLastUrlHash();
if (!IS_MAC_OS) {
window.removeMenu();
}
if (IS_WINDOWS && tray.isEnabled) {
tray.setupListeners(window);
tray.create();
}
window.webContents.once('dom-ready', async () => {
processDeeplink();
if (IS_PRODUCTION) {
setupAutoUpdates(windowState);
}
if (!IS_FIRST_RUN && getIsAutoUpdateEnabled() === undefined) {
store.set(AUTO_UPDATE_SETTING_KEY, true);
await captureLocalStorage();
reloadWindows();
}
window.show();
});
windows.add(window);
loadWindowUrl(window, url, windowState.urlHash);
}
function loadWindowUrl(window: BrowserWindow, url?: string, hash?: string): void {
if (url && checkIsWebContentsUrlAllowed(url)) {
window.loadURL(url);
} else if (!app.isPackaged) {
window.loadURL(`http://localhost:1234${hash}`);
window.webContents.openDevTools();
} else if (getIsAutoUpdateEnabled()) {
window.loadURL(`${process.env.BASE_URL}${hash}`);
} else if (getIsAutoUpdateEnabled() === undefined && IS_FIRST_RUN) {
store.set(AUTO_UPDATE_SETTING_KEY, true);
window.loadURL(`${process.env.BASE_URL}${hash}`);
} else {
window.loadURL(`file://${__dirname}/index.html${hash}`);
}
}
export function setupElectronActionHandlers() {
ipcMain.handle(ElectronAction.OPEN_NEW_WINDOW, (_, url: string) => {
createWindow(url);
});
ipcMain.handle(ElectronAction.SET_WINDOW_TITLE, (_, newTitle?: string) => {
getCurrentWindow()?.setTitle(getAppTitle(newTitle));
});
ipcMain.handle(ElectronAction.GET_IS_FULLSCREEN, () => {
getCurrentWindow()?.isFullScreen();
});
ipcMain.handle(ElectronAction.HANDLE_DOUBLE_CLICK, () => {
const currentWindow = getCurrentWindow();
const doubleClickAction = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
if (doubleClickAction === 'Minimize') {
currentWindow?.minimize();
} else if (doubleClickAction === 'Maximize') {
if (!currentWindow?.isMaximized()) {
currentWindow?.maximize();
} else {
currentWindow?.unmaximize();
}
}
});
ipcMain.handle(ElectronAction.SET_WINDOW_BUTTONS_POSITION, (_, position: WindowButtonsPosition) => {
if (!IS_MAC_OS) {
return;
}
getCurrentWindow()?.setWindowButtonPosition(WINDOW_BUTTONS_POSITION[position]);
});
ipcMain.handle(ElectronAction.SET_IS_AUTO_UPDATE_ENABLED, async (_, isAutoUpdateEnabled: boolean) => {
if (IS_PREVIEW) {
return;
}
store.set(AUTO_UPDATE_SETTING_KEY, isAutoUpdateEnabled);
await captureLocalStorage();
reloadWindows(isAutoUpdateEnabled);
});
ipcMain.handle(ElectronAction.GET_IS_AUTO_UPDATE_ENABLED, () => {
return getIsAutoUpdateEnabled();
});
ipcMain.handle(ElectronAction.SET_IS_TRAY_ICON_ENABLED, (_, isTrayIconEnabled: boolean) => {
if (isTrayIconEnabled) {
tray.enable();
} else {
tray.disable();
}
});
ipcMain.handle(ElectronAction.GET_IS_TRAY_ICON_ENABLED, () => tray.isEnabled);
ipcMain.handle(ElectronAction.RESTORE_LOCAL_STORAGE, () => restoreLocalStorage());
}
export function setupCloseHandlers() {
app.on('window-all-closed', () => {
if (!IS_MAC_OS) {
app.quit();
}
});
app.on('before-quit', (event) => {
if (IS_MAC_OS && !forceQuit.isEnabled) {
event.preventDefault();
forceQuit.enable();
app.quit();
}
});
app.on('activate', () => {
const hasActiveWindow = BrowserWindow.getAllWindows().length > 0;
if (!hasActiveWindow) {
createWindow();
} else if (IS_MAC_OS) {
forceQuit.disable();
getLastWindow()?.show();
}
});
}

View File

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

View File

@ -1,7 +1,6 @@
import type { ActionReturnType } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_ELECTRON } from '../../../util/browser/windowEnvironment';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { createMessageHashUrl } from '../../../util/routing';
import { addActionHandler, setGlobal } from '../../index';
@ -93,11 +92,7 @@ addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnTyp
const hashUrl = createMessageHashUrl(chatId, 'thread', threadId);
if (IS_ELECTRON) {
window.electron!.openNewWindow(hashUrl);
} else {
window.open(hashUrl, '_blank');
}
window.open(hashUrl, '_blank');
});
addActionHandler('openPreviousChat', (global, actions, payload): ActionReturnType => {

View File

@ -4,9 +4,9 @@ import type { LangCode } from '../../../types';
import type { ActionReturnType, GlobalState } from '../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { IS_MULTIACCOUNT_SUPPORTED } from '../../../util/browser/globalEnvironment';
import { IS_MULTIACCOUNT_SUPPORTED, IS_TAURI } from '../../../util/browser/globalEnvironment';
import {
IS_ANDROID, IS_ELECTRON, IS_IOS, IS_LINUX,
IS_ANDROID, IS_IOS, IS_LINUX,
IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV, IS_WINDOWS,
} from '../../../util/browser/windowEnvironment';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -171,8 +171,8 @@ addCallback((global: GlobalState) => {
if (IS_SAFARI) {
document.body.classList.add('is-safari');
}
if (IS_ELECTRON) {
document.body.classList.add('is-electron');
if (IS_TAURI) {
document.body.classList.add('is-tauri');
}
});

View File

@ -7,7 +7,8 @@ import {
ANIMATION_WAVE_MIN_INTERVAL,
DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
} from '../../../config';
import { IS_ELECTRON, IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnvironment';
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
import { IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnvironment';
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
import { getAllNotificationsCount } from '../../../util/folderManager';
import generateUniqueId from '../../../util/generateUniqueId';
@ -754,15 +755,6 @@ addActionHandler('checkAppVersion', (global): ActionReturnType => {
});
});
addActionHandler('setIsElectronUpdateAvailable', (global, action, payload): ActionReturnType => {
global = getGlobal();
global = {
...global,
isElectronUpdateAvailable: Boolean(payload.isAvailable),
};
setGlobal(global);
});
addActionHandler('afterHangUp', (global): ActionReturnType => {
if (!selectTabState(global, getCurrentTabId()).multitabNextAction) return;
reestablishMasterToSelf();
@ -811,7 +803,8 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
return;
}
if (global.initialUnreadNotifications && Math.round(Date.now() / 1000) % 2 === 0) {
// Show blinking title in browser tab
if (!IS_TAURI && global.initialUnreadNotifications && Math.round(Date.now() / 1000) % 2 === 0) {
const notificationCount = getAllNotificationsCount();
const newUnread = notificationCount - global.initialUnreadNotifications;
@ -843,7 +836,7 @@ addActionHandler('updatePageTitle', (global, actions, payload): ActionReturnType
}
}
setPageTitleInstant(IS_ELECTRON ? '' : `${prefix}${PAGE_TITLE}`);
setPageTitleInstant(`${prefix}${PAGE_TITLE}`);
});
addActionHandler('closeInviteViaLinkModal', (global, actions, payload): ActionReturnType => {

View File

@ -100,7 +100,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
passcode: {},
twoFaSettings: {},
isAppUpdateAvailable: false,
isElectronUpdateAvailable: false,
shouldShowContextMenuHint: true,
appConfig: DEFAULT_APP_CONFIG,

View File

@ -1063,7 +1063,6 @@ export interface ActionPayloads {
openLimitReachedModal: { limit: ApiLimitTypeWithModal } & WithTabId;
closeLimitReachedModal: WithTabId | undefined;
checkAppVersion: undefined;
setIsElectronUpdateAvailable: { isAvailable: boolean };
setGlobalSearchClosing: ({
isClosing?: boolean;
} & WithTabId) | undefined;

View File

@ -92,7 +92,6 @@ export type GlobalState = {
isSyncing?: boolean;
isAppConfigLoaded?: boolean;
isAppUpdateAvailable?: boolean;
isElectronUpdateAvailable?: boolean;
isSynced?: boolean;
isFetchingDifference?: boolean;
leftColumnWidth?: number;

View File

@ -2,6 +2,7 @@ import type { ElementRef } from '../../lib/teact/teact';
import { useEffect, useRef } from '../../lib/teact/teact';
import { forceMutation, requestMutation } from '../../lib/fasterdom/fasterdom';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_IOS, IS_SAFARI } from '../../util/browser/windowEnvironment';
import { stopScrollInertia } from '../../util/resetScroll';
import useDebouncedCallback from '../useDebouncedCallback';
@ -33,7 +34,7 @@ export default function useTopOverscroll(
overscrollTriggerRef.current.style.display = 'block';
containerRef.current.scrollTop = TRIGGER_HEIGHT;
if (!IS_SAFARI && !noScrollInertiaStop) {
if (!IS_SAFARI && !noScrollInertiaStop && !IS_TAURI) {
stopScrollInertia(containerRef.current);
}

View File

@ -0,0 +1,35 @@
import { useCallback, useEffect } from '../../lib/teact/teact';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
const NO_DRAG_ELEMENTS = 'input, a, button';
const useTauriDrag = () => {
const handleMouseDown = useCallback(async (event: MouseEvent) => {
if (!(event.target instanceof HTMLElement)) {
return;
}
if (event.target?.closest(NO_DRAG_ELEMENTS)) {
return;
}
if (event.target?.closest('[data-tauri-drag-region]')) {
const tauriWindow = await window.tauri?.getCurrentWindow();
tauriWindow?.startDragging();
}
}, []);
useEffect(() => {
if (!(IS_TAURI && IS_MAC_OS)) return undefined;
document.addEventListener('mousedown', handleMouseDown);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
};
}, [handleMouseDown]);
};
export default useTauriDrag;

View File

@ -0,0 +1,30 @@
import type { Event } from '@tauri-apps/api/event';
import { useEffect } from '../../lib/teact/teact';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
export default function useTauriEvent<T>(name: string, callback: (event: Event<T>) => void) {
return useEffect(() => {
if (!IS_TAURI) {
return undefined;
}
let removeListener: VoidFunction | undefined;
const setUpListener = async () => {
const { listen } = await import('@tauri-apps/api/event');
removeListener = await listen<T>(name, (event) => {
callback(event);
});
};
setUpListener().catch((error) => {
// eslint-disable-next-line no-console
console.error(`Could not set up window event listener. ${error}`);
});
return () => {
removeListener?.();
};
}, [name, callback]);
}

View File

@ -8,7 +8,8 @@ import { SERVICE_NOTIFICATIONS_USER_ID } from '../config';
import {
getCanDeleteChat, isChatArchived, isChatChannel, isChatGroup,
} from '../global/helpers';
import { IS_ELECTRON, IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
import { IS_TAURI } from '../util/browser/globalEnvironment';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
import { isUserId } from '../util/entities/ids';
import { compact } from '../util/iteratees';
import useLang from './useLang';
@ -87,7 +88,7 @@ const useChatContextActions = ({
} = getActions();
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
title: IS_ELECTRON ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'),
title: IS_TAURI ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'),
icon: 'open-in-new-tab',
handler: () => {
if (isSavedDialog) {

View File

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

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, useUnmountCleanup } from '../lib/teact/teact';
import { useRef, useUnmountCleanup } from '../lib/teact/teact';
import { ELECTRON_WINDOW_DRAG_EVENT_START } from '../config';
import useLastCallback from './useLastCallback';
const DEFAULT_THRESHOLD = 250;
@ -52,14 +51,6 @@ function useLongPress({
window.clearTimeout(timerId.current);
});
useEffect(() => {
document.body.addEventListener(ELECTRON_WINDOW_DRAG_EVENT_START, cancel);
return () => {
document.body.removeEventListener(ELECTRON_WINDOW_DRAG_EVENT_START, cancel);
};
}, []);
return {
onMouseDown: start,
onMouseUp: end,

View File

@ -1,8 +1,7 @@
import type { ElementRef } from '../../lib/teact/teact';
import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact';
import { ElectronEvent } from '../../types/electron';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_IOS } from '../../util/browser/windowEnvironment';
type ReturnType = [boolean, () => void, () => void] | [false];
@ -90,15 +89,27 @@ export const useFullscreenStatus = () => {
setIsFullscreen(checkIfFullscreen());
};
const removeElectronListener = window.electron?.on(ElectronEvent.FULLSCREEN_CHANGE, setIsFullscreen);
window.electron?.isFullscreen().then(setIsFullscreen);
let removeTauriListener: VoidFunction | undefined;
const setupTauriListener = async () => {
const tauriWindow = await window.tauri?.getCurrentWindow();
removeTauriListener = await tauriWindow.onResized(() => {
tauriWindow.isFullscreen().then(setIsFullscreen);
});
};
if (IS_TAURI) {
window.tauri?.getCurrentWindow().then((tauriWindow) => {
tauriWindow.isFullscreen().then(setIsFullscreen);
});
setupTauriListener();
}
document.addEventListener('fullscreenchange', listener, false);
document.addEventListener('webkitfullscreenchange', listener, false);
document.addEventListener('mozfullscreenchange', listener, false);
return () => {
removeElectronListener?.();
removeTauriListener?.();
document.removeEventListener('fullscreenchange', listener, false);
document.removeEventListener('webkitfullscreenchange', listener, false);

View File

@ -14,6 +14,7 @@ import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom';
import { selectTabState } from './global/selectors';
import { selectSharedSettings } from './global/selectors/sharedState';
import { betterView } from './util/betterView';
import { IS_TAURI } from './util/browser/globalEnvironment';
import { requestGlobal, subscribeToMultitabBroadcastChannel } from './util/browser/multitab';
import { establishMultitabRole, subscribeToMasterChange } from './util/establishMultitabRole';
import { initGlobal } from './util/init';
@ -21,6 +22,8 @@ import { initLocalization } from './util/localization';
import { MULTITAB_STORAGE_KEY } from './util/multiaccount';
import { checkAndAssignPermanentWebVersion } from './util/permanentWebVersion';
import { onBeforeUnload } from './util/schedulers';
import initTauriApi from './util/tauri/initTauriApi';
import setupTauriListeners from './util/tauri/setupTauriListeners';
import updateWebmanifest from './util/updateWebmanifest';
import App from './components/App';
@ -32,6 +35,11 @@ if (STRICTERDOM_ENABLED) {
enableStrict();
}
if (IS_TAURI) {
initTauriApi();
setupTauriListeners();
}
init();
async function init() {
@ -44,8 +52,6 @@ async function init() {
checkAndAssignPermanentWebVersion();
await window.electron?.restoreLocalStorage();
subscribeToMultitabBroadcastChannel();
await requestGlobal(APP_VERSION);
localStorage.setItem(MULTITAB_STORAGE_KEY, '1');

View File

@ -1250,10 +1250,11 @@ class TelegramClient {
}
}
async start(authParams: UserAuthParams) {
async start(authParams: UserAuthParams, onConnected?: NoneToVoidFunction) {
if (!this.isConnected()) {
await this.connect();
}
onConnected?.();
this.loadConfig();

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { DEBUG, ELECTRON_HOST_URL, IS_PACKAGED_ELECTRON } from '../config';
import { DEBUG } from '../config';
import { pause } from '../util/schedulers';
import { clearAssetCache, respondWithCache, respondWithCacheNetworkFirst } from './assetCache';
import { respondForDownload } from './download';
@ -47,7 +47,7 @@ self.addEventListener('activate', (e) => {
self.addEventListener('fetch', (e: FetchEvent) => {
const { url } = e.request;
const scope = IS_PACKAGED_ELECTRON ? ELECTRON_HOST_URL : self.registration.scope;
const { scope } = self.registration;
if (!url.startsWith(scope)) {
return false;
}

View File

@ -218,6 +218,7 @@ $color-message-story-mention-to: #74bcff;
--border-radius-forum-avatar: 33.3333%;
--messages-container-width: 45.5rem;
--right-column-width: 26.5rem;
--window-controls-width: 0rem;
--header-height: 3.5rem;
--custom-emoji-size: 1.25rem;
--emoji-size: 1.25rem;

View File

@ -92,8 +92,9 @@ body.is-ios {
--border-radius-messages-small: 0.5rem;
}
body.is-electron {
body.is-tauri {
--custom-cursor: default;
--window-controls-width: 5rem;
}
body.cursor-grabbing {

View File

@ -1,48 +0,0 @@
export enum ElectronEvent {
FULLSCREEN_CHANGE = 'fullscreen-change',
UPDATE_ERROR = 'update-error',
UPDATE_AVAILABLE = 'update-available',
DEEPLINK = 'deeplink',
}
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',
SET_WINDOW_TITLE = 'set-window-title',
SET_WINDOW_BUTTONS_POSITION = 'set-window-buttons-position',
SET_IS_AUTO_UPDATE_ENABLED = 'set-is-auto-update-enabled',
GET_IS_AUTO_UPDATE_ENABLED = 'get-is-auto-update-enabled',
SET_IS_TRAY_ICON_ENABLED = 'set-is-tray-icon-enabled',
GET_IS_TRAY_ICON_ENABLED = 'get-is-tray-icon-enabled',
RESTORE_LOCAL_STORAGE = 'restore-local-storage',
}
export type WindowButtonsPosition = 'standard' | 'lowered';
export interface ElectronApi {
isFullscreen: () => Promise<boolean>;
installUpdate: () => Promise<void>;
handleDoubleClick: () => Promise<void>;
openNewWindow: (url: string, title?: string) => Promise<void>;
setWindowTitle: (title?: string) => Promise<void>;
setWindowButtonsPosition: (position: WindowButtonsPosition) => Promise<void>;
/**
* @deprecated Use `setWindowButtonsPosition` instead
*/
setTrafficLightPosition: (position: WindowButtonsPosition) => Promise<void>;
setIsAutoUpdateEnabled: (value: boolean) => Promise<void>;
getIsAutoUpdateEnabled: () => Promise<boolean>;
setIsTrayIconEnabled: (value: boolean) => Promise<void>;
getIsTrayIconEnabled: () => Promise<boolean>;
restoreLocalStorage: () => Promise<void>;
on: (eventName: ElectronEvent, callback: any) => VoidFunction;
}
declare global {
interface Window {
electron?: ElectronApi;
}
}

View File

@ -1647,6 +1647,7 @@ export interface LangPair {
'ToDoListErrorChooseTitle': undefined;
'ToDoListErrorChooseTasks': undefined;
'PremiumPreviewTodo': undefined;
'NativeDownloadFailed': undefined;
'DescriptionAboutTon': undefined;
'ButtonTopUpViaFragment': undefined;
'TonModalHint': undefined;

21
src/types/tauri.ts Normal file
View File

@ -0,0 +1,21 @@
import type { Window as TauriWindow } from '@tauri-apps/api/window';
import type { Update } from '@tauri-apps/plugin-updater';
type TauriApi = {
version: string;
markTitleBarOverlay: (isOverlay: boolean) => Promise<void>;
setNotificationsCount: (amount: number, isMuted?: boolean) => Promise<void>;
openNewWindow: (url: string) => Promise<void>;
relaunch: () => Promise<void>;
checkUpdate: () => Promise<Update | null>;
getCurrentWindow: () => Promise<TauriWindow>;
setWindowTitle: (title: string) => Promise<void>;
};
declare global {
interface Window {
tauri: TauriApi;
}
}
export {};

View File

@ -2,8 +2,14 @@ import { getGlobal } from '../global';
import { DEBUG } from '../config';
import { selectTabState } from '../global/selectors';
import { IS_TAURI } from './browser/globalEnvironment';
export function updateAppBadge(unreadCount: number, isMuted?: boolean) {
if (IS_TAURI) {
window.tauri?.setNotificationsCount?.(unreadCount, isMuted);
return;
}
export function updateAppBadge(unreadCount: number) {
if (!selectTabState(getGlobal()).isMasterTab) return;
if (typeof window.navigator.setAppBadge !== 'function') {
return;

View File

@ -1,6 +1,10 @@
import { isTauri } from '@tauri-apps/api/core';
declare const globalThis: ServiceWorkerGlobalScope & WorkerGlobalScope & SharedWorkerGlobalScope & Window;
export const IS_MULTIACCOUNT_SUPPORTED = 'SharedWorker' in globalThis;
export const IS_INTL_LIST_FORMAT_SUPPORTED = 'ListFormat' in Intl;
export const IS_BAD_URL_PARSER = new URL('tg://host').host !== 'host';
export const ARE_WEBCODECS_SUPPORTED = 'VideoDecoder' in globalThis;
export const IS_TAURI = isTauri();

View File

@ -1,4 +1,5 @@
import { IS_TEST, PRODUCTION_HOSTNAME } from '../../config';
import { IS_TAURI } from './globalEnvironment';
export function getPlatform() {
const { userAgent, platform } = window.navigator;
@ -36,7 +37,6 @@ export const IS_YA_BROWSER = navigator.userAgent.includes('YaBrowser');
export const IS_FIREFOX = navigator.userAgent.toLowerCase().includes('firefox')
|| navigator.userAgent.toLowerCase().includes('iceweasel')
|| navigator.userAgent.toLowerCase().includes('icecat');
export const IS_ELECTRON = Boolean(window.electron);
export const MouseButton = {
Main: 0,
@ -52,7 +52,7 @@ export const IS_PWA = (
|| document.referrer.includes('android-app://')
);
export const IS_APP = IS_PWA || IS_ELECTRON;
export const IS_APP = IS_PWA || IS_TAURI;
export const IS_TOUCH_ENV = window.matchMedia('(pointer: coarse)').matches;
export const IS_VOICE_RECORDING_SUPPORTED = Boolean(

View File

@ -1,4 +1,3 @@
import { ELECTRON_HOST_URL, IS_PACKAGED_ELECTRON } from '../config';
import { ACCOUNT_SLOT } from './multiaccount';
const cacheApi = self.caches;
@ -30,9 +29,7 @@ export async function fetch(
try {
// To avoid the error "Request scheme 'webdocument' is unsupported"
const request = IS_PACKAGED_ELECTRON
? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}`
: new Request(key.replace(/:/g, '_'));
const request = new Request(key.replace(/:/g, '_'));
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
const response = await cache.match(request);
if (!response) {
@ -90,9 +87,7 @@ 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 = IS_PACKAGED_ELECTRON
? `${ELECTRON_HOST_URL}/${key.replace(/:/g, '_')}`
: new Request(key.replace(/:/g, '_'));
const request = new Request(key.replace(/:/g, '_'));
const response = new Response(cacheData);
const cache = await cacheApi.open(`${cacheName}${SUFFIX}`);
await cache.put(request, response);

View File

@ -1,3 +1,4 @@
import { IS_TAURI } from './browser/globalEnvironment';
import { createCallbackManager } from './callbacks';
import { ESTABLISH_BROADCAST_CHANNEL_NAME } from './multiaccount';
import { getPasscodeHash, setPasscodeHash } from './passcode';
@ -17,6 +18,7 @@ const initialEstablishment = new Deferred();
let masterToken: number | undefined;
let isWaitingForMaster = false;
let reestablishToken: number | undefined;
let isChannelClosed = false;
type EstablishMessage = {
collectedTokens: Set<number>;
@ -136,6 +138,7 @@ const handleMessage = ({ data }: { data: EstablishMessage }) => {
};
export function establishMultitabRole(shouldReestablishMasterToSelf?: boolean) {
if (isChannelClosed) return;
channel.addEventListener('message', handleMessage);
channel.postMessage({ collectedTokens });
@ -153,13 +156,16 @@ export function establishMultitabRole(shouldReestablishMasterToSelf?: boolean) {
}, ESTABLISH_TIMEOUT);
window.addEventListener('beforeunload', signalTokenDead);
if (IS_TAURI) window.addEventListener('unload', signalTokenDead);
}
export function signalTokenDead() {
if (isChannelClosed) return;
runCallbacksTokenDied(token);
channel.removeEventListener('message', handleMessage);
channel.postMessage({ tokenDied: token, currentPasscodeHash: getPasscodeHash() });
channel.close();
isChannelClosed = true;
}
export function signalPasscodeHash() {

View File

@ -8,8 +8,8 @@ import {
} from '../api/types';
import {
DEBUG, ELECTRON_HOST_URL,
IS_PACKAGED_ELECTRON, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS,
DEBUG, MEDIA_CACHE_DISABLED, MEDIA_CACHE_NAME,
MEDIA_CACHE_NAME_AVATARS,
} from '../config';
import { callApi, cancelApiProgress } from '../api/gramjs';
import {
@ -27,7 +27,7 @@ const asCacheApiType = {
[ApiMediaFormat.Progressive]: undefined,
};
const PROGRESSIVE_URL_PREFIX = `${IS_PACKAGED_ELECTRON ? ELECTRON_HOST_URL : '.'}/progressive/`;
const PROGRESSIVE_URL_PREFIX = './progressive/';
const DOWNLOAD_URL_PREFIX = './download/';
const MAX_MEDIA_RETRIES = 5;

Some files were not shown because too many files have changed in this diff Show More