fix(security+audit): strip latent RCE grants, opener allowlist, GDI leaks, CI hardening
From the deep-audit wave (reviewer-verified: capability identifiers valid, no removed-crate references, GDI free ordering correct): - Removed 8 never-registered plugins (clipboard-manager, fs, shell, http, process, os, dialog, global-shortcut) from Cargo.toml AND their capability grants (shell:allow-execute, unscoped fs writes, http:default, …) — verified the web never invokes any of them. A latent RCE-class surface is gone. - on_new_window: only http/https/mailto reach the OS opener (file:///custom schemes previously bypassed the opener capability scope entirely). - set_badge_count: freed hdc + hdc_screen on all three GDI error paths (leaked per badge update in a long-running tray app). - 8s reveal failsafe gated by an AtomicBool: no longer re-shows a window the user closed to tray; page-load reveal now fires once only (logout reloads don't re-surface a tray-hidden window); recovery for a missed page-load event preserved. - toast.rs: store pruned on Activated too + capped at 20 (was unbounded). - Startup no longer panics when the bundled icon is missing (tray skipped gracefully); msSmartScreenProtection no longer disabled (throttling disables kept); rust-version corrected to 1.77.2. - release.yml update-manifest: fails on empty signatures (was: could publish a manifest that traps Windows users in a failed-update loop); partial- failure window documented. Deleted the stale upstream tauri.yml workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,14 @@ jobs:
|
|||||||
id: release
|
id: release
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
# NOTE (partial-failure window): this renames the `latest` release's
|
||||||
|
# name/body to the new version up front, before the platform builds run.
|
||||||
|
# If a build later fails, the release keeps the OLD binary assets but the
|
||||||
|
# NEW name. That's cosmetic: the auto-updater reads release.json, which is
|
||||||
|
# only regenerated by the update-manifest job — and that job `needs:` BOTH
|
||||||
|
# build-windows and build-linux, so a failed/skipped build prevents any
|
||||||
|
# manifest (and therefore updater) change. Clients keep the last good
|
||||||
|
# release.json until a fully successful run replaces it.
|
||||||
run: |
|
run: |
|
||||||
VERSION="4.12.${{ github.run_number }}"
|
VERSION="4.12.${{ github.run_number }}"
|
||||||
EXISTING=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/tags/latest" \
|
EXISTING=$(curl -sf "$GITEA_URL/api/v1/repos/$REPO/releases/tags/latest" \
|
||||||
@@ -277,6 +285,13 @@ jobs:
|
|||||||
|
|
||||||
WIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64-setup.nsis.zip.sig")
|
WIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64-setup.nsis.zip.sig")
|
||||||
LIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64.AppImage.tar.gz.sig")
|
LIN_SIG=$(curl -sf "$BASE/LotusChat-x86_64.AppImage.tar.gz.sig")
|
||||||
|
|
||||||
|
# Never publish a manifest with a missing/empty signature: the updater
|
||||||
|
# would reject (or worse, accept an unsigned) artifact. Fail the job so
|
||||||
|
# the previous good release.json stays in place.
|
||||||
|
[ -n "$WIN_SIG" ] || { echo "ERROR: empty Windows signature" >&2; exit 1; }
|
||||||
|
[ -n "$LIN_SIG" ] || { echo "ERROR: empty Linux signature" >&2; exit 1; }
|
||||||
|
|
||||||
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
python3 -c "import json,sys; v,d,wu,ws,lu,ls=sys.argv[1:]; print(json.dumps({'version':v,'notes':'Latest Lotus Chat release','pub_date':d,'platforms':{'windows-x86_64':{'url':wu,'signature':ws},'linux-x86_64':{'url':lu,'signature':ls}}},indent=2))" \
|
python3 -c "import json,sys; v,d,wu,ws,lu,ls=sys.argv[1:]; print(json.dumps({'version':v,'notes':'Latest Lotus Chat release','pub_date':d,'platforms':{'windows-x86_64':{'url':wu,'signature':ws},'linux-x86_64':{'url':lu,'signature':ls}}},indent=2))" \
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
name: "Publish Tauri App"
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Windows-x86_64
|
|
||||||
windows-x86_64:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".node-version"
|
|
||||||
package-manager-cache: false
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable # They use branch based releases
|
|
||||||
- name: Install cinny dependencies
|
|
||||||
run: cd cinny && npm ci
|
|
||||||
- name: Install tauri dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Build desktop app with Tauri
|
|
||||||
uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
- name: Get app version (windows)
|
|
||||||
run: |
|
|
||||||
$json = (Get-Content "src-tauri\tauri.conf.json" -Raw) | ConvertFrom-Json
|
|
||||||
$version = $json.version
|
|
||||||
echo "Version: ${version}"
|
|
||||||
echo "TAURI_VERSION=${version}" >> $Env:GITHUB_ENV
|
|
||||||
echo "${Env:TAURI_VERSION}"
|
|
||||||
shell: pwsh
|
|
||||||
- name: Move msi
|
|
||||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi"
|
|
||||||
shell: pwsh
|
|
||||||
- name: Move msi.zip
|
|
||||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi.zip" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi.zip"
|
|
||||||
shell: pwsh
|
|
||||||
- name: Move msi.zip.sig
|
|
||||||
run: Move-Item "src-tauri\target\release\bundle\msi\Cinny_${{ env.TAURI_VERSION }}_x64_en-US.msi.zip.sig" "src-tauri\target\release\bundle\msi\Cinny_desktop-x86_64.msi.zip.sig"
|
|
||||||
shell: pwsh
|
|
||||||
- name: Upload tagged release
|
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi
|
|
||||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi.zip
|
|
||||||
src-tauri/target/release/bundle/msi/Cinny_desktop-x86_64.msi.zip.sig
|
|
||||||
|
|
||||||
# Linux-x86_64
|
|
||||||
linux-x86_64:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".node-version"
|
|
||||||
package-manager-cache: false
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable # They use branch based releases
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
- name: Install cinny dependencies
|
|
||||||
run: cd cinny && npm ci
|
|
||||||
- name: Install tauri dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Build desktop app with Tauri
|
|
||||||
uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
- name: Get app version
|
|
||||||
id: vars
|
|
||||||
run: echo "tag=$(jq .version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
|
|
||||||
- name: Move deb
|
|
||||||
run: mv "src-tauri/target/release/bundle/deb/Cinny_${{ steps.vars.outputs.tag }}_amd64.deb" "src-tauri/target/release/bundle/deb/Cinny_desktop-x86_64.deb"
|
|
||||||
- name: Move AppImage
|
|
||||||
run: mv "src-tauri/target/release/bundle/appimage/Cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage"
|
|
||||||
- name: Move AppImage.tar.gz
|
|
||||||
run: mv "src-tauri/target/release/bundle/appimage/Cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage.tar.gz" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz"
|
|
||||||
- name: Move AppImage.tar.gz.sig
|
|
||||||
run: mv "src-tauri/target/release/bundle/appimage/Cinny_${{ steps.vars.outputs.tag }}_amd64.AppImage.tar.gz.sig" "src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz.sig"
|
|
||||||
- name: Upload tagged release
|
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
src-tauri/target/release/bundle/deb/Cinny_desktop-x86_64.deb
|
|
||||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage
|
|
||||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz
|
|
||||||
src-tauri/target/release/bundle/appimage/Cinny_desktop-x86_64.AppImage.tar.gz.sig
|
|
||||||
|
|
||||||
# macos-universal
|
|
||||||
macos-universal:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".node-version"
|
|
||||||
package-manager-cache: false
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable # They use branch based releases
|
|
||||||
with:
|
|
||||||
targets: aarch64-apple-darwin,x86_64-apple-darwin
|
|
||||||
- name: Install cinny dependencies
|
|
||||||
run: cd cinny && npm ci
|
|
||||||
- name: Install tauri dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Build desktop app with Tauri
|
|
||||||
uses: tauri-apps/tauri-action@73fb865345c54760d875b94642314f8c0c894afa # v0.6.1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
||||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
|
||||||
with:
|
|
||||||
args: "--target universal-apple-darwin"
|
|
||||||
- name: Get app version
|
|
||||||
id: vars
|
|
||||||
run: echo "tag=$(jq .version src-tauri/tauri.conf.json | tr -d '"')" >> $GITHUB_OUTPUT
|
|
||||||
- name: Move dmg
|
|
||||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_${{ steps.vars.outputs.tag }}_universal.dmg" "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_desktop-universal.dmg"
|
|
||||||
- name: Move app.tar.gz
|
|
||||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny.app.tar.gz" "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz"
|
|
||||||
- name: Move app.tar.gz.sig
|
|
||||||
run: mv "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny.app.tar.gz.sig" "src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz.sig"
|
|
||||||
- name: Upload tagged release
|
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
src-tauri/target/universal-apple-darwin/release/bundle/dmg/Cinny_desktop-universal.dmg
|
|
||||||
src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz
|
|
||||||
src-tauri/target/universal-apple-darwin/release/bundle/macos/Cinny_desktop-universal.app.tar.gz.sig
|
|
||||||
|
|
||||||
# Upload release.json
|
|
||||||
release-update:
|
|
||||||
if: always()
|
|
||||||
needs: [windows-x86_64, linux-x86_64, macos-universal]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
- name: Run release.json
|
|
||||||
run: npm run release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
+2
-10
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "cinny"
|
name = "cinny"
|
||||||
version = "4.12.2"
|
version = "4.12.2" # CI patches src-tauri/tauri.conf.json at build time; that file is the source of truth for the shipped version.
|
||||||
description = "Yet another matrix client"
|
description = "Yet another matrix client"
|
||||||
authors = ["Ajay Bura"]
|
authors = ["Ajay Bura"]
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
repository = "https://github.com/cinnyapp/cinny-desktop"
|
repository = "https://github.com/cinnyapp/cinny-desktop"
|
||||||
default-run = "cinny"
|
default-run = "cinny"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.61"
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
@@ -20,14 +20,7 @@ serde = { version = "1.0.193", features = ["derive"] }
|
|||||||
tauri = { version = "2", features = ["devtools", "wry", "tray-icon", "image-png"] }
|
tauri = { version = "2", features = ["devtools", "wry", "tray-icon", "image-png"] }
|
||||||
tauri-plugin-localhost = "2"
|
tauri-plugin-localhost = "2"
|
||||||
tauri-plugin-window-state = "2"
|
tauri-plugin-window-state = "2"
|
||||||
tauri-plugin-clipboard-manager = "2"
|
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
tauri-plugin-fs = "2"
|
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
tauri-plugin-http = "2"
|
|
||||||
tauri-plugin-process = "2"
|
|
||||||
tauri-plugin-os = "2"
|
|
||||||
tauri-plugin-dialog = "2"
|
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
|
|
||||||
@@ -40,7 +33,6 @@ default = [ "custom-protocol" ]
|
|||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"global-shortcut:default",
|
|
||||||
"deep-link:default"
|
"deep-link:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -12,15 +12,6 @@
|
|||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"fs:allow-read-file",
|
|
||||||
"fs:allow-write-file",
|
|
||||||
"fs:allow-read-dir",
|
|
||||||
"fs:allow-copy-file",
|
|
||||||
"fs:allow-mkdir",
|
|
||||||
"fs:allow-remove",
|
|
||||||
"fs:allow-remove",
|
|
||||||
"fs:allow-rename",
|
|
||||||
"fs:allow-exists",
|
|
||||||
"core:window:allow-create",
|
"core:window:allow-create",
|
||||||
"core:window:allow-center",
|
"core:window:allow-center",
|
||||||
"core:window:allow-request-user-attention",
|
"core:window:allow-request-user-attention",
|
||||||
@@ -54,35 +45,9 @@
|
|||||||
"core:window:allow-set-ignore-cursor-events",
|
"core:window:allow-set-ignore-cursor-events",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:webview:allow-print",
|
"core:webview:allow-print",
|
||||||
"shell:allow-execute",
|
|
||||||
"shell:allow-open",
|
|
||||||
"dialog:allow-open",
|
|
||||||
"dialog:allow-save",
|
|
||||||
"dialog:allow-message",
|
|
||||||
"dialog:allow-ask",
|
|
||||||
"dialog:allow-confirm",
|
|
||||||
"http:default",
|
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"global-shortcut:allow-is-registered",
|
|
||||||
"global-shortcut:allow-register",
|
|
||||||
"global-shortcut:allow-register-all",
|
|
||||||
"global-shortcut:allow-unregister",
|
|
||||||
"global-shortcut:allow-unregister-all",
|
|
||||||
"os:allow-platform",
|
|
||||||
"os:allow-version",
|
|
||||||
"os:allow-os-type",
|
|
||||||
"os:allow-family",
|
|
||||||
"os:allow-arch",
|
|
||||||
"os:allow-exe-extension",
|
|
||||||
"os:allow-locale",
|
|
||||||
"os:allow-hostname",
|
|
||||||
"process:allow-restart",
|
|
||||||
"process:allow-exit",
|
|
||||||
"clipboard-manager:allow-read-text",
|
|
||||||
"clipboard-manager:allow-write-text",
|
|
||||||
"core:app:allow-app-show",
|
"core:app:allow-app-show",
|
||||||
"core:app:allow-app-hide",
|
"core:app:allow-app-hide",
|
||||||
"clipboard-manager:default",
|
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-open-url",
|
"identifier": "opener:allow-open-url",
|
||||||
"allow": [{ "url": "http://*" }, { "url": "https://*" }]
|
"allow": [{ "url": "http://*" }, { "url": "https://*" }]
|
||||||
|
|||||||
+86
-44
@@ -196,8 +196,14 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
|
let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
|
||||||
let hbm_color =
|
let hbm_color =
|
||||||
CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0)
|
match CreateDIBSection(Some(hdc), &bmi, DIB_RGB_COLORS, &mut bits, None, 0) {
|
||||||
.map_err(|e| e.to_string())?;
|
Ok(bm) => bm,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = DeleteDC(hdc);
|
||||||
|
let _ = ReleaseDC(None, hdc_screen);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
// Zero-init so undrawn pixels are fully transparent (CreateDIBSection
|
// Zero-init so undrawn pixels are fully transparent (CreateDIBSection
|
||||||
// does not guarantee zeroed memory; garbage bytes cause a black square).
|
// does not guarantee zeroed memory; garbage bytes cause a black square).
|
||||||
if !bits.is_null() {
|
if !bits.is_null() {
|
||||||
@@ -263,6 +269,8 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
|||||||
let hbm_mask = CreateBitmap(size, size, 1, 1, None);
|
let hbm_mask = CreateBitmap(size, size, 1, 1, None);
|
||||||
if hbm_mask.0 as usize == 0 {
|
if hbm_mask.0 as usize == 0 {
|
||||||
let _ = DeleteObject(hbm_color.into());
|
let _ = DeleteObject(hbm_color.into());
|
||||||
|
let _ = DeleteDC(hdc);
|
||||||
|
let _ = ReleaseDC(None, hdc_screen);
|
||||||
return Err("CreateBitmap failed".to_string());
|
return Err("CreateBitmap failed".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,6 +284,8 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
|
|||||||
let hicon = CreateIconIndirect(&icon_info).map_err(|e| {
|
let hicon = CreateIconIndirect(&icon_info).map_err(|e| {
|
||||||
let _ = DeleteObject(hbm_color.into());
|
let _ = DeleteObject(hbm_color.into());
|
||||||
let _ = DeleteObject(hbm_mask.into());
|
let _ = DeleteObject(hbm_mask.into());
|
||||||
|
let _ = DeleteDC(hdc);
|
||||||
|
let _ = ReleaseDC(None, hdc_screen);
|
||||||
e.to_string()
|
e.to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -437,51 +447,54 @@ pub fn run() {
|
|||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
// --- System tray: keeps Lotus Chat running in the background so
|
// --- System tray: keeps Lotus Chat running in the background so
|
||||||
// notifications keep arriving after the window is closed-to-tray. ---
|
// notifications keep arriving after the window is closed-to-tray. ---
|
||||||
let base_icon = app.default_window_icon().cloned();
|
// Degrade gracefully if the bundled icon is missing rather than
|
||||||
let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
|
// panicking at startup (the tray simply isn't created).
|
||||||
let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
|
if let Some(base_icon) = app.default_window_icon().cloned() {
|
||||||
let separator = PredefinedMenuItem::separator(app)?;
|
let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
|
||||||
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
|
let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
|
||||||
let tray = TrayIconBuilder::with_id("main-tray")
|
let separator = PredefinedMenuItem::separator(app)?;
|
||||||
.icon(base_icon.clone().expect("bundled window icon"))
|
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
|
||||||
.tooltip("Lotus Chat")
|
let tray = TrayIconBuilder::with_id("main-tray")
|
||||||
.menu(&tray_menu)
|
.icon(base_icon.clone())
|
||||||
.show_menu_on_left_click(false)
|
.tooltip("Lotus Chat")
|
||||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
.menu(&tray_menu)
|
||||||
"open" => show_main(app),
|
.show_menu_on_left_click(false)
|
||||||
"quit" => app.exit(0),
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
_ => {}
|
"open" => show_main(app),
|
||||||
})
|
"quit" => app.exit(0),
|
||||||
.on_tray_icon_event(|tray, event| {
|
_ => {}
|
||||||
if let TrayIconEvent::Click {
|
})
|
||||||
button: MouseButton::Left,
|
.on_tray_icon_event(|tray, event| {
|
||||||
button_state: MouseButtonState::Up,
|
if let TrayIconEvent::Click {
|
||||||
..
|
button: MouseButton::Left,
|
||||||
} = event
|
button_state: MouseButtonState::Up,
|
||||||
{
|
..
|
||||||
let app = tray.app_handle();
|
} = event
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
{
|
||||||
if window.is_visible().unwrap_or(false) {
|
let app = tray.app_handle();
|
||||||
let _ = window.hide();
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
} else {
|
if window.is_visible().unwrap_or(false) {
|
||||||
show_main(app);
|
let _ = window.hide();
|
||||||
|
} else {
|
||||||
|
show_main(app);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
.build(app)?;
|
||||||
.build(app)?;
|
|
||||||
|
|
||||||
// Keep the tray handle (and base icon pixels) in managed state so
|
// Keep the tray handle (and base icon pixels) in managed state so
|
||||||
// set_tray_unread can re-render the icon at runtime.
|
// set_tray_unread can re-render the icon at runtime.
|
||||||
if let Some(img) = base_icon {
|
let base_rgba = base_icon.rgba().to_vec();
|
||||||
let base_rgba = img.rgba().to_vec();
|
let (width, height) = (base_icon.width(), base_icon.height());
|
||||||
let (width, height) = (img.width(), img.height());
|
|
||||||
app.manage(TrayUnreadState {
|
app.manage(TrayUnreadState {
|
||||||
tray,
|
tray,
|
||||||
base_rgba,
|
base_rgba,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
eprintln!("tray: no bundled window icon; skipping system tray setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -494,6 +507,12 @@ pub fn run() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
// Tracks whether the window's visibility has already been decided:
|
||||||
|
// set true by the on_page_load reveal (window shown) and by the
|
||||||
|
// close-to-tray handler (window intentionally hidden). The 8s failsafe
|
||||||
|
// below only reveals the window if neither of those has happened.
|
||||||
|
let window_settled = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let settled_page_load = window_settled.clone();
|
||||||
let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url)
|
let window = WebviewWindowBuilder::new(app, "main".to_string(), window_url)
|
||||||
.title("Lotus Chat")
|
.title("Lotus Chat")
|
||||||
// First-run defaults; tauri-plugin-window-state restores geometry
|
// First-run defaults; tauri-plugin-window-state restores geometry
|
||||||
@@ -513,15 +532,30 @@ pub fn run() {
|
|||||||
// appends the Chromium background-throttling disables. Windows-only
|
// appends the Chromium background-throttling disables. Windows-only
|
||||||
// in effect; harmless elsewhere. Does not block system sleep.
|
// in effect; harmless elsewhere. Does not block system sleep.
|
||||||
.additional_browser_args(
|
.additional_browser_args(
|
||||||
"--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows",
|
"--disable-features=msWebOOUI,msPdfOOUI --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows",
|
||||||
)
|
)
|
||||||
.on_page_load(|window, payload| {
|
.on_page_load(move |window, payload| {
|
||||||
if matches!(payload.event(), PageLoadEvent::Finished) {
|
if matches!(payload.event(), PageLoadEvent::Finished) {
|
||||||
let _ = window.show();
|
// Reveal only on the FIRST settle: later page loads (e.g. a
|
||||||
|
// logout reload) must not re-show a window the user has
|
||||||
|
// since closed to the tray.
|
||||||
|
if !settled_page_load.swap(true, std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
let _ = window.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_new_window(move |url, _features| {
|
.on_new_window(move |url, _features| {
|
||||||
let _ = app_handle.opener().open_url(url.as_str(), None::<&str>);
|
// Only hand well-known web/mail schemes to the OS opener.
|
||||||
|
// Forwarding arbitrary schemes (file://, custom protocols)
|
||||||
|
// bypasses the opener capability scope and reaches the OS.
|
||||||
|
match url.scheme() {
|
||||||
|
"http" | "https" | "mailto" => {
|
||||||
|
let _ = app_handle.opener().open_url(url.as_str(), None::<&str>);
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
eprintln!("opener: refusing to open URL with scheme '{other}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
NewWindowResponse::Deny
|
NewWindowResponse::Deny
|
||||||
})
|
})
|
||||||
.build()?;
|
.build()?;
|
||||||
@@ -529,19 +563,27 @@ pub fn run() {
|
|||||||
// Close-to-tray: hide instead of exiting; the app is quit explicitly
|
// Close-to-tray: hide instead of exiting; the app is quit explicitly
|
||||||
// from the tray menu.
|
// from the tray menu.
|
||||||
let window_for_close = window.clone();
|
let window_for_close = window.clone();
|
||||||
|
let settled_close = window_settled.clone();
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
|
// Mark the window state as settled so the failsafe below can't
|
||||||
|
// re-show a window the user just closed to the tray.
|
||||||
|
settled_close.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
let _ = window_for_close.hide();
|
let _ = window_for_close.hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Failsafe: never leave the window stuck hidden if the page-load
|
// Failsafe: never leave the window stuck hidden if the page-load
|
||||||
// event doesn't fire for some reason.
|
// reveal never fires. Skips if the window state was already settled
|
||||||
|
// (revealed on page load, or intentionally hidden to the tray).
|
||||||
let window_for_show = window.clone();
|
let window_for_show = window.clone();
|
||||||
|
let settled_failsafe = window_settled.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(8));
|
std::thread::sleep(std::time::Duration::from_secs(8));
|
||||||
let _ = window_for_show.show();
|
if !settled_failsafe.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
|
let _ = window_for_show.show();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deep links (matrix:): route both the cold-start case and the
|
// Deep links (matrix:): route both the cold-start case and the
|
||||||
|
|||||||
@@ -158,7 +158,15 @@ fn show_windows_toast(
|
|||||||
let room_id_owned = room_id.map(|s| s.to_string());
|
let room_id_owned = room_id.map(|s| s.to_string());
|
||||||
let path_owned = path.map(|s| s.to_string());
|
let path_owned = path.map(|s| s.to_string());
|
||||||
let activated = TypedEventHandler::<ToastNotification, IInspectable>::new(
|
let activated = TypedEventHandler::<ToastNotification, IInspectable>::new(
|
||||||
move |_sender, args| {
|
move |sender, args| {
|
||||||
|
// Activation means this toast is done; drop it from the keep-alive
|
||||||
|
// store now. A Dismissed event doesn't reliably fire for a toast the
|
||||||
|
// user activated, so pruning only on Dismissed would leak it.
|
||||||
|
if let Some(sender) = sender.as_ref() {
|
||||||
|
if let Ok(mut store) = toast_store().lock() {
|
||||||
|
store.retain(|t| t != sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some(args) = args.as_ref() else {
|
let Some(args) = args.as_ref() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
@@ -204,9 +212,16 @@ fn show_windows_toast(
|
|||||||
);
|
);
|
||||||
let _ = toast.Dismissed(&dismissed)?;
|
let _ = toast.Dismissed(&dismissed)?;
|
||||||
|
|
||||||
// Keep the toast (and its handlers) alive until dismissed.
|
// Keep the toast (and its handlers) alive until dismissed/activated.
|
||||||
if let Ok(mut store) = toast_store().lock() {
|
if let Ok(mut store) = toast_store().lock() {
|
||||||
store.push(toast.clone());
|
store.push(toast.clone());
|
||||||
|
// Hard cap: if some Dismissed/Activated events are missed, retain only
|
||||||
|
// the most recent 20 toasts (dropping the oldest) so the store can't
|
||||||
|
// grow unbounded for the app's lifetime.
|
||||||
|
let len = store.len();
|
||||||
|
if len > 20 {
|
||||||
|
store.drain(0..len - 20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No AUMID argument: relies on the process AppUserModelID (see module note).
|
// No AUMID argument: relies on the process AppUserModelID (see module note).
|
||||||
|
|||||||
Reference in New Issue
Block a user