Compare commits

..

7 Commits

Author SHA1 Message Date
byGalax 6301ebb392 chore(desktop): release v0.10.2 2026-04-22 19:24:14 +02:00
byGalax 31d21dd2c2 fix(release): tauri v2 ships .exe + .exe.sig, not .nsis.zip
v1 used to wrap the installer in a .nsis.zip and sign that wrapper.
v2 signs the .exe directly, so the updater url points at the .exe and
the .sig file sits next to it. Script was still looking for the
legacy .nsis.zip path and aborting after a successful build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:22:17 +02:00
byGalax 9add0a4d61 fix(release): call tauri binary directly, not via desktop build script
Node execSync runs via cmd.exe on Windows, which preserves the `--`
separator pnpm injects between the script name and forwarded args.
Tauri CLI then forwards that `--` to cargo, which rejects `--bundles`
with "unexpected argument". Invoking `pnpm exec tauri build --bundles nsis`
skips the script indirection so no `--` is emitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:19:42 +02:00
byGalax 500f1c4bc2 chore(release): self-hosted updater on update.netralax.cloud
Switches the Tauri updater endpoint from GitHub Releases to a static
host. New Ed25519 pubkey (old private key was lost); existing 0.10.x
installs need one manual reinstall to pick up the new updater identity.

Release flow is now pnpm release <version> <notes> which bumps,
builds + signs locally, scps artifacts to the server, commits, tags.
GitHub workflow stays as workflow_dispatch backup (Windows only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:58 +02:00
byGalax a38e2f96c0 feat(desktop): raise ringtone cap to 8 MB
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:17:48 +02:00
byGalax 5aa39b40ff feat(desktop): Windows taskbar overlay icon for unread badge
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled
Red-dot overlay drawn as raw RGBA (no extra resource bundled).
Shown on Windows when unread count > 0, cleared when 0.
macOS keeps numeric Dock badge; Linux has no cross-DE badge API.

Bumps version 0.10.0 -> 0.10.1.
2026-04-21 17:31:06 +02:00
byGalax eb452bf57e fix(desktop): gate set_badge_label behind macOS cfg
Windows/Linux WebviewWindow have no set_badge_label method —
build failed on GitHub Actions windows runner.
2026-04-21 17:26:22 +02:00
14 changed files with 254 additions and 41 deletions
+21
View File
@@ -0,0 +1,21 @@
# Copy to .env.release (gitignored) and fill in.
# Consumed by scripts/release.mjs.
# Absolute path to the private key file produced by `tauri signer generate`.
TAURI_SIGNING_PRIVATE_KEY_PATH=C:/Users/denni/.tauri/chatapp.key
# Password set when generating the key. Leave empty if none.
TAURI_SIGNING_PRIVATE_KEY_PASSWORD=
# Host serving latest.json + installer artifacts over HTTPS.
UPDATE_HOST=update.netralax.cloud
# SSH user on UPDATE_HOST with write access to UPDATE_REMOTE_PATH.
UPDATE_SSH_USER=chatapp-deploy
# Optional: path to the SSH private key. Omit to fall back on ssh-agent or the
# default id_rsa.
UPDATE_SSH_KEY=
# Absolute path on the server where windows/ artifacts + latest.json live.
UPDATE_REMOTE_PATH=/var/www/updates/windows
+16 -28
View File
@@ -1,35 +1,27 @@
name: Release desktop app
name: Release desktop app (manual backup)
# Tag a version to trigger a release:
# git tag v0.1.0 && git push --tags
#
# Produces signed Tauri bundles for macOS (arm + intel), Windows, and Linux,
# uploads them to a GitHub Release, and publishes `latest.json` for the
# updater plugin to discover.
# Self-hosted updates run from scripts/release.mjs on Dennis's Windows box.
# This workflow is kept as a manual backup — trigger it from the Actions tab
# if the local build host is unavailable.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to build (e.g. v0.10.2) — must already exist"
required: true
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: macos-14 # universal binary covers both Intel + Apple Silicon
args: "--target universal-apple-darwin --bundles app,updater"
- platform: windows-latest
args: ""
runs-on: ${{ matrix.platform }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -42,8 +34,6 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: Install JS deps
run: pnpm install --frozen-lockfile
@@ -54,18 +44,16 @@ jobs:
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 }}
# Client-side env vars baked into the bundle — paste your prod values
# into the repo's Actions → Secrets so releases point at prod.
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_AUTH_REDIRECT_URL: ${{ secrets.VITE_AUTH_REDIRECT_URL }}
VITE_LIVEKIT_URL: ${{ secrets.VITE_LIVEKIT_URL }}
with:
projectPath: apps/desktop
tagName: ${{ github.ref_name }}
releaseName: "ChatApp ${{ github.ref_name }}"
releaseBody: "See the assets below to download this version."
tagName: ${{ inputs.tag }}
releaseName: "ChatApp ${{ inputs.tag }}"
releaseBody: "Manual build — copy .nsis.zip/.sig/latest.json to the update host."
releaseDraft: true
prerelease: false
tauriScript: pnpm exec tauri
args: ${{ matrix.args }}
args: "--bundles nsis"
+2
View File
@@ -15,7 +15,9 @@ out/
.env
.env.local
.env.*.local
.env.release
!.env.example
!.env.release.example
# Expo
.expo/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@chat-app/desktop",
"version": "0.10.0",
"version": "0.10.2",
"private": true,
"description": "Tauri v2 desktop client (Windows / macOS / Linux)",
"type": "module",
+1 -1
View File
@@ -726,7 +726,7 @@ dependencies = [
[[package]]
name = "chat-app-desktop"
version = "0.10.0"
version = "0.10.1"
dependencies = [
"base64 0.22.1",
"dryoc",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chat-app-desktop"
version = "0.10.0"
version = "0.10.2"
description = "ChatApp desktop client"
authors = ["Dennis"]
edition = "2021"
+43 -2
View File
@@ -22,6 +22,33 @@ struct TrayUnreadPayload {
count: u32,
}
// Red-dot overlay icon for the Windows taskbar. Drawn as raw RGBA instead of
// shipping a PNG so we don't add another resource to the bundle. Kept small
// (32x32) since Windows scales the overlay down anyway.
#[cfg(target_os = "windows")]
fn unread_overlay_rgba() -> Vec<u8> {
const SIZE: u32 = 32;
let r = SIZE as f32 / 2.0;
let mut buf = Vec::with_capacity((SIZE * SIZE * 4) as usize);
for y in 0..SIZE {
for x in 0..SIZE {
let dx = x as f32 - r + 0.5;
let dy = y as f32 - r + 0.5;
let d = (dx * dx + dy * dy).sqrt();
let edge = r - 1.0;
if d <= edge {
buf.extend_from_slice(&[0xDC, 0x26, 0x26, 0xFF]);
} else if d <= r {
let alpha = (255.0 * (r - d)).clamp(0.0, 255.0) as u8;
buf.extend_from_slice(&[0xDC, 0x26, 0x26, alpha]);
} else {
buf.extend_from_slice(&[0, 0, 0, 0]);
}
}
}
buf
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn show_main_window(app: &AppHandle) {
if let Some(win) = app.get_webview_window("main") {
@@ -171,8 +198,10 @@ pub fn run() {
format!("ChatApp · {} neu", payload.count)
};
let _ = tray_handle.set_tooltip(Some(tooltip));
// macOS dock badge. `set_badge_label` is macOS-only but the
// call is a no-op on other platforms so we don't need a cfg.
// Dock/taskbar badge. macOS uses a numeric label; Windows uses
// an overlay icon (red dot = unread). Linux has no cross-DE
// badge API — skip.
#[cfg(target_os = "macos")]
if let Some(win) = badge_window.as_ref() {
let badge = if payload.count == 0 {
None
@@ -181,6 +210,18 @@ pub fn run() {
};
let _ = win.set_badge_label(badge);
}
#[cfg(target_os = "windows")]
if let Some(win) = badge_window.as_ref() {
if payload.count == 0 {
let _ = win.set_overlay_icon(None);
} else {
let rgba = unread_overlay_rgba();
let img = tauri::image::Image::new_owned(rgba, 32, 32);
let _ = win.set_overlay_icon(Some(img));
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let _ = &badge_window;
});
Ok(())
+5 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ChatApp",
"version": "0.10.0",
"version": "0.10.2",
"identifier": "com.meinname.chatapp",
"build": {
"beforeDevCommand": "pnpm vite:dev",
@@ -42,8 +42,10 @@
},
"plugins": {
"updater": {
"endpoints": ["https://github.com/byGalax/chat-app/releases/latest/download/latest.json"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ5N0U0RDcxOTU2OEQ0QUUKUldTdTFHaVZjVTErU1ZuMk1lWXBUbEcyS1RHYzJQN3k4VDdiUGRvRnVJYVJKR3BxWG1xcENpdlYK",
"endpoints": [
"https://update.netralax.cloud/windows/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI1Mzc0QjVBQUZEQTA3RUIKUldUckI5cXZXa3MzdGM3QkE4WWFPd3NnVzRZeXdpcUM0eUtjRDlGN09ySEdzNXhLNlo3azBPajYK",
"windows": {
"installMode": "passive"
}
@@ -82,7 +82,7 @@ export function RingtoneSettings({ disabled = false }: Props) {
if (code === 'ringtone_too_large') {
setError(
t('app:settings.ringtone_error_too_large', {
defaultValue: 'Datei zu groß (max 2 MB).',
defaultValue: 'Datei zu groß (max {{max}} MB).',
max: MAX_RINGTONE_BYTES / BYTES_PER_MB,
}),
);
@@ -236,7 +236,7 @@ export function RingtoneSettings({ disabled = false }: Props) {
<p className="text-[11px] text-fg-muted">
{t('app:settings.ringtone_hint', {
defaultValue:
'MP3, WAV, OGG oder M4A bis 2 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.',
'MP3, WAV, OGG oder M4A bis 8 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.',
})}
</p>
</div>
+1 -1
View File
@@ -16,7 +16,7 @@ export interface StoredRingtone {
updatedAt: number;
}
export const MAX_RINGTONE_BYTES = 2 * 1024 * 1024; // 2 MB cap
export const MAX_RINGTONE_BYTES = 8 * 1024 * 1024; // 8 MB cap
export const SUPPORTED_RINGTONE_MIMES = [
'audio/mpeg',
+1
View File
@@ -26,6 +26,7 @@
"desktop": "pnpm --filter @chat-app/desktop",
"desktop:dev": "pnpm --filter @chat-app/desktop dev",
"desktop:build": "pnpm --filter @chat-app/desktop build",
"release": "node scripts/release.mjs",
"db:types": "supabase gen types typescript --local > packages/db-types/src/index.ts",
"prod:migrate": "./scripts/prod/push-migrations.sh",
"prod:deploy-fn": "./scripts/prod/push-edge-function.sh",
+1 -1
View File
@@ -205,7 +205,7 @@
"ringtone_preview": "Vorhören",
"ringtone_stop": "Stop",
"ringtone_reset": "Zurücksetzen",
"ringtone_hint": "MP3, WAV, OGG oder M4A bis 2 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.",
"ringtone_hint": "MP3, WAV, OGG oder M4A bis 8 MB. Nur für eingehende Anrufe — ausgehend bleibt der Standard.",
"ringtone_error_too_large": "Datei zu groß (max {{max}} MB).",
"ringtone_error_not_audio": "Nur Audio-Dateien werden unterstützt.",
"ringtone_error_generic": "Ringtone konnte nicht gespeichert werden.",
+1 -1
View File
@@ -205,7 +205,7 @@
"ringtone_preview": "Preview",
"ringtone_stop": "Stop",
"ringtone_reset": "Reset",
"ringtone_hint": "MP3, WAV, OGG or M4A up to 2 MB. Incoming calls only — outgoing keeps the default.",
"ringtone_hint": "MP3, WAV, OGG or M4A up to 8 MB. Incoming calls only — outgoing keeps the default.",
"ringtone_error_too_large": "File too large (max {{max}} MB).",
"ringtone_error_not_audio": "Only audio files are supported.",
"ringtone_error_generic": "Could not save the ringtone.",
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env node
// Release script — builds the Windows desktop installer, signs it with the
// Tauri updater key, and uploads the artifacts + latest.json to the update
// host over scp. Reads credentials from .env.release (not committed).
//
// Usage:
// pnpm release 0.10.2 "Ringtone cap auf 8 MB, bugfixes"
import { execSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');
const [, , versionArg, ...notesParts] = process.argv;
if (!versionArg || !/^\d+\.\d+\.\d+$/.test(versionArg)) {
console.error('Usage: pnpm release <x.y.z> "release notes"');
process.exit(1);
}
const notes = notesParts.join(' ').trim() || `Release ${versionArg}`;
const envPath = join(ROOT, '.env.release');
if (!existsSync(envPath)) {
console.error('.env.release missing. Copy .env.release.example and fill it in.');
process.exit(1);
}
const env = Object.fromEntries(
readFileSync(envPath, 'utf8')
.split('\n')
.map((l) => l.trim())
.filter((l) => l && !l.startsWith('#'))
.map((l) => {
const i = l.indexOf('=');
return [l.slice(0, i).trim(), l.slice(i + 1).trim()];
}),
);
const required = [
'TAURI_SIGNING_PRIVATE_KEY_PATH',
'UPDATE_HOST',
'UPDATE_SSH_USER',
'UPDATE_REMOTE_PATH',
];
for (const key of required) {
if (!env[key]) {
console.error(`Missing ${key} in .env.release`);
process.exit(1);
}
}
if (!existsSync(env.TAURI_SIGNING_PRIVATE_KEY_PATH)) {
console.error(`Signing key not found at ${env.TAURI_SIGNING_PRIVATE_KEY_PATH}`);
process.exit(1);
}
const gitStatus = execSync('git status --porcelain', { cwd: ROOT, encoding: 'utf8' });
if (gitStatus.trim()) {
console.error('Working tree not clean. Commit or stash first.');
console.error(gitStatus);
process.exit(1);
}
const pkgJsonPath = join(ROOT, 'apps/desktop/package.json');
const tauriConfPath = join(ROOT, 'apps/desktop/src-tauri/tauri.conf.json');
const cargoTomlPath = join(ROOT, 'apps/desktop/src-tauri/Cargo.toml');
function bumpJson(path, version) {
const obj = JSON.parse(readFileSync(path, 'utf8'));
obj.version = version;
writeFileSync(path, JSON.stringify(obj, null, 2) + '\n', 'utf8');
}
function bumpCargo(path, version) {
const text = readFileSync(path, 'utf8');
const next = text.replace(/^version = "[^"]+"$/m, `version = "${version}"`);
if (next === text) throw new Error(`No version line found in ${path}`);
writeFileSync(path, next, 'utf8');
}
bumpJson(pkgJsonPath, versionArg);
bumpJson(tauriConfPath, versionArg);
bumpCargo(cargoTomlPath, versionArg);
console.log(`Version -> ${versionArg}. Building NSIS bundle…`);
const signingKey = readFileSync(env.TAURI_SIGNING_PRIVATE_KEY_PATH, 'utf8').trim();
const buildEnv = {
...process.env,
TAURI_SIGNING_PRIVATE_KEY: signingKey,
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ?? '',
};
// Call the tauri binary directly (not via the desktop `build` script) so the
// `--` separator pnpm normally injects doesn't get forwarded to cargo. When
// run from cmd.exe (Node's default execSync shell on Windows) pnpm preserves
// the `--`, which cargo then rejects with "unexpected argument '--bundles'".
execSync('pnpm --filter @chat-app/desktop exec tauri build --bundles nsis', {
cwd: ROOT,
env: buildEnv,
stdio: 'inherit',
});
// Tauri v2 ships a single .exe + .exe.sig for NSIS updates — no .nsis.zip
// wrapper like v1. The updater downloads the .exe directly, verifies the
// minisign signature, then launches it in passive mode.
const bundleDir = join(ROOT, 'apps/desktop/src-tauri/target/release/bundle/nsis');
const exeName = `ChatApp_${versionArg}_x64-setup.exe`;
const sigName = `${exeName}.sig`;
const exePath = join(bundleDir, exeName);
const sigPath = join(bundleDir, sigName);
for (const p of [exePath, sigPath]) {
if (!existsSync(p)) {
console.error(`Missing build artifact: ${p}`);
process.exit(1);
}
}
const signature = readFileSync(sigPath, 'utf8').trim();
const latest = {
version: versionArg,
notes,
pub_date: new Date().toISOString(),
platforms: {
'windows-x86_64': {
signature,
url: `https://${env.UPDATE_HOST}/windows/${exeName}`,
},
},
};
const latestJsonPath = join(bundleDir, 'latest.json');
writeFileSync(latestJsonPath, JSON.stringify(latest, null, 2), 'utf8');
const sshTarget = `${env.UPDATE_SSH_USER}@${env.UPDATE_HOST}`;
const keyFlag = env.UPDATE_SSH_KEY ? ` -i "${env.UPDATE_SSH_KEY}"` : '';
function scp(localPath) {
execSync(`scp${keyFlag} "${localPath}" ${sshTarget}:${env.UPDATE_REMOTE_PATH}/`, {
cwd: ROOT,
stdio: 'inherit',
});
}
console.log('Uploading artifacts (JSON last so clients never see a stale ref)…');
scp(exePath);
scp(sigPath);
scp(latestJsonPath);
execSync(
`git add apps/desktop/package.json apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/Cargo.toml`,
{ cwd: ROOT, stdio: 'inherit' },
);
execSync(`git commit -m "chore(desktop): release v${versionArg}"`, {
cwd: ROOT,
stdio: 'inherit',
});
execSync(`git tag v${versionArg}`, { cwd: ROOT, stdio: 'inherit' });
console.log(`\nReleased v${versionArg}`);
console.log(` Manifest: https://${env.UPDATE_HOST}/windows/latest.json`);
console.log(` Installer: https://${env.UPDATE_HOST}/windows/${exeName}`);
console.log(` Run 'git push && git push --tags' to sync to remote.`);