Replace Electron with Tauri
This commit is contained in:
parent
d57a572ff8
commit
5e32fad59e
19
.eslintignore
Normal file
19
.eslintignore
Normal 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
|
||||
400
.github/workflows/package-and-publish.yml
vendored
400
.github/workflows/package-and-publish.yml
vendored
@ -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"
|
||||
|
||||
71
README.md
71
README.md
@ -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))
|
||||
|
||||
25
deploy/prepareTauriConfig.js
Normal file
25
deploy/prepareTauriConfig.js
Normal 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
13
deploy/tauri_create_dmg.sh
Executable 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
184
docs/TAURI.md
Normal 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)
|
||||
@ -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
7554
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@ -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 |
@ -1 +1 @@
|
||||
10.9.72
|
||||
10.9.71
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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.";
|
||||
|
||||
@ -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);
|
||||
}, []);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
height: 1rem;
|
||||
|
||||
:global(body.is-electron) & {
|
||||
:global(body.is-tauri) & {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,10 +45,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
&.customBgImage.blurred::before {
|
||||
filter: blur(12px);
|
||||
}
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
92
src/components/middle/MobileSearch.scss
Normal file
92
src/components/middle/MobileSearch.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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 (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
@ -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}://`));
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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;
|
||||
},
|
||||
};
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
@ -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 => {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -100,7 +100,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
passcode: {},
|
||||
twoFaSettings: {},
|
||||
isAppUpdateAvailable: false,
|
||||
isElectronUpdateAvailable: false,
|
||||
shouldShowContextMenuHint: true,
|
||||
appConfig: DEFAULT_APP_CONFIG,
|
||||
|
||||
|
||||
@ -1063,7 +1063,6 @@ export interface ActionPayloads {
|
||||
openLimitReachedModal: { limit: ApiLimitTypeWithModal } & WithTabId;
|
||||
closeLimitReachedModal: WithTabId | undefined;
|
||||
checkAppVersion: undefined;
|
||||
setIsElectronUpdateAvailable: { isAvailable: boolean };
|
||||
setGlobalSearchClosing: ({
|
||||
isClosing?: boolean;
|
||||
} & WithTabId) | undefined;
|
||||
|
||||
@ -92,7 +92,6 @@ export type GlobalState = {
|
||||
isSyncing?: boolean;
|
||||
isAppConfigLoaded?: boolean;
|
||||
isAppUpdateAvailable?: boolean;
|
||||
isElectronUpdateAvailable?: boolean;
|
||||
isSynced?: boolean;
|
||||
isFetchingDifference?: boolean;
|
||||
leftColumnWidth?: number;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
35
src/hooks/tauri/useTauriDrag.ts
Normal file
35
src/hooks/tauri/useTauriDrag.ts
Normal 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;
|
||||
30
src/hooks/tauri/useTauriEvent.ts
Normal file
30
src/hooks/tauri/useTauriEvent.ts
Normal 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]);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
2
src/lib/gramjs/tl/api.d.ts
vendored
2
src/lib/gramjs/tl/api.d.ts
vendored
File diff suppressed because one or more lines are too long
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
1
src/types/language.d.ts
vendored
1
src/types/language.d.ts
vendored
@ -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
21
src/types/tauri.ts
Normal 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 {};
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user