Skip to content

Commit 05511e9

Browse files
committed
fix: android setup
1 parent 0d4d913 commit 05511e9

10 files changed

Lines changed: 119 additions & 53 deletions

File tree

package.json

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "forwardemail-webmail-client",
33
"version": "0.2.17",
4-
""private": true,
4+
"private": true,
55
"type": "module",
66
"packageManager": "pnpm@9.0.0",
77
"engines": {
@@ -10,6 +10,7 @@
1010
},
1111
"scripts": {
1212
"dev": "vite",
13+
"tauri": "tauri",
1314
"prebuild": "node scripts/validate-schema-version.js",
1415
"build": "vite build && workbox generateSW workbox.config.cjs",
1516
"preview": "vite preview",
@@ -134,10 +135,6 @@
134135
"vitest": "^2.1.4",
135136
"workbox-cli": "^7.0.0"
136137
},
137-
"engines": {
138-
"node": ">=20 <21",
139-
"pnpm": ">=9.0.0"
140-
},
141138
"lint-staged": {
142139
"*.{js,mjs,cjs,ts,tsx,jsx,svelte}": "pnpm run lint",
143140
"*.{json,css,html,md,svelte}": "pnpm format"
@@ -146,34 +143,8 @@
146143
"branch": "main",
147144
"publish": false
148145
},
149-
"packageManager": "pnpm@9.0.0",
150-
"private": true,
151146
"repository": {
152147
"type": "git",
153148
"url": "https://github.com/forwardemail/mail.forwardemail.net.git"
154-
},
155-
"scripts": {
156-
"analyze": "ANALYZE=true pnpm build",
157-
"build": "vite build && workbox generateSW workbox.config.cjs",
158-
"check": "svelte-check",
159-
"dev": "vite",
160-
"format": "prettier . --check",
161-
"format:fix": "prettier . --write",
162-
"lhci": "pnpm build && pnpm lhci:collect && pnpm lhci:assert",
163-
"lhci:assert": "lhci assert --config=./lighthouserc.cjs",
164-
"lhci:collect": "lhci collect --config=./lighthouserc.cjs",
165-
"lint": "eslint .",
166-
"lint-staged": "lint-staged",
167-
"lint:fix": "eslint . --fix",
168-
"prebuild": "node scripts/validate-schema-version.js",
169-
"prepare": "husky install",
170-
"preview": "vite preview",
171-
"release": "np",
172-
"test": "vitest",
173-
"test:coverage": "vitest run --coverage",
174-
"test:e2e": "playwright test",
175-
"test:unit": "vitest",
176-
"test:watch": "vitest watch"
177-
},
178-
"type": "module"
149+
}
179150
}

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/capabilities/default.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,14 @@
2222
"notification:allow-notify",
2323
"notification:allow-create-channel",
2424

25-
"updater:allow-check",
26-
"updater:allow-download-and-install",
27-
2825
"deep-link:default",
2926

3027
"process:allow-exit",
3128
"process:allow-restart",
3229

3330
"opener:allow-open-url",
3431

35-
"log:allow-log",
36-
37-
"window-state:default"
38-
]
32+
"log:allow-log"
33+
],
34+
"platforms": ["linux", "macOS", "windows", "android", "iOS"]
3935
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v2.0.0/crates/tauri-cli/capabilities-schema.json",
3+
"identifier": "desktop",
4+
"description": "Desktop-only permissions for plugins not available on mobile (updater, window-state).",
5+
"windows": ["main"],
6+
"permissions": [
7+
"updater:allow-check",
8+
"updater:allow-download-and-install",
9+
"window-state:default"
10+
],
11+
"platforms": ["linux", "macOS", "windows"]
12+
}

src-tauri/src/lib.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use serde::Serialize;
2+
use tauri::{Emitter, Listener, Manager};
3+
4+
#[cfg(desktop)]
25
use tauri::{
36
menu::{Menu, MenuItem},
47
tray::TrayIconBuilder,
5-
Emitter, Listener, Manager,
68
};
79

810
// ── Payload types ────────────────────────────────────────────────────────────
@@ -60,23 +62,25 @@ fn set_badge_count(count: u32) -> Result<(), String> {
6062

6163
/// Shows or hides the main window (for tray icon toggle).
6264
/// Only operates on the "main" window label — never arbitrary windows.
65+
#[cfg(desktop)]
6366
#[tauri::command]
6467
fn toggle_window_visibility(app: tauri::AppHandle) -> Result<(), String> {
6568
let window = app
6669
.get_webview_window("main")
6770
.ok_or_else(|| "Main window not found".to_string())?;
6871

6972
if window.is_visible().unwrap_or(false) {
70-
window.hide().map_err(|e| e.to_string())?;
73+
window.hide().map_err(|e: tauri::Error| e.to_string())?;
7174
} else {
72-
window.show().map_err(|e| e.to_string())?;
73-
window.set_focus().map_err(|e| e.to_string())?;
75+
window.show().map_err(|e: tauri::Error| e.to_string())?;
76+
window.set_focus().map_err(|e: tauri::Error| e.to_string())?;
7477
}
7578
Ok(())
7679
}
7780

7881
// ── Tray Icon ────────────────────────────────────────────────────────────────
7982

83+
#[cfg(desktop)]
8084
fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
8185
let show = MenuItem::with_id(app, "show", "Show Forward Email", true, None::<&str>)?;
8286
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
@@ -165,6 +169,7 @@ pub fn run() {
165169
get_app_version,
166170
get_platform,
167171
set_badge_count,
172+
#[cfg(desktop)]
168173
toggle_window_visibility,
169174
])
170175
.setup(|app| {

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './polyfills';
12
import * as mailboxActions from './stores/mailboxActions';
23
import { createStarfield } from './utils/starfield';
34
import { Local, Accounts, reconcileOrphanedAccountData } from './utils/storage';

src/polyfills.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Polyfills for older WebViews (Android API 30 / Chrome <85–98)
2+
3+
if (typeof globalThis.structuredClone === 'undefined') {
4+
(globalThis as Record<string, unknown>).structuredClone = (obj: unknown) =>
5+
JSON.parse(JSON.stringify(obj));
6+
}
7+
8+
if (typeof String.prototype.replaceAll === 'undefined') {
9+
String.prototype.replaceAll = function (search: string | RegExp, replacement: string): string {
10+
if (search instanceof RegExp) {
11+
if (!search.global) {
12+
throw new TypeError('String.prototype.replaceAll called with a non-global RegExp');
13+
}
14+
return this.replace(search, replacement);
15+
}
16+
return this.split(search).join(replacement);
17+
};
18+
}

src/stores/mailboxStore.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,24 @@ const createMailboxStore = () => {
11011101
merged = await mergeMissingFrom(account, merged);
11021102
}
11031103

1104+
// Guard against transient empty responses: if the server returns zero
1105+
// messages for a non-search, non-filtered page-1 request but we already
1106+
// have cached data, keep the cache instead of clearing the inbox.
1107+
// This handles intermittent backend storage issues (e.g. stale reads
1108+
// from distributed SQLite) that briefly return empty result sets.
1109+
const isBasicPage1 =
1110+
!shouldAppend &&
1111+
currentPage === 1 &&
1112+
!queryParam &&
1113+
!get(unreadOnly) &&
1114+
!get(hasAttachmentsOnly);
1115+
if (isBasicPage1 && !merged.length && cachedPage.length) {
1116+
tracer.end({ status: 'transient_empty_kept_cache', cachedCount: cachedPage.length });
1117+
loading.set(false);
1118+
error.set('');
1119+
return;
1120+
}
1121+
11041122
// Always prune stale cache entries on page 1 when we have fresh server data
11051123
// This ensures deleted/moved messages don't reappear from cache
11061124
const shouldPrune = !shouldAppend && currentPage === 1 && cachedPage.length && merged.length;

src/svelte/Mailbox.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5234,7 +5234,7 @@ const stopVerticalResize = () => {
52345234
{@const msgList = $filteredMessages}
52355235
{#each msgList as msg}
52365236
<article
5237-
class={`relative cursor-pointer hover:bg-accent/50 transition-colors ${($selectedConversationIds || []).includes(msg.id) ? 'bg-accent' : ''}`}
5237+
class={`relative cursor-pointer hover:bg-accent/50 transition-colors ${$selectedMessage?.id === msg.id || ($selectedConversationIds || []).includes(msg.id) ? 'bg-accent' : ''}`}
52385238
oncontextmenu={(e) => openContextMenu(e, msg)}
52395239
ondblclick={(e) => {
52405240
if (isDraftMessage(msg)) {

src/utils/inactivity-timer.js

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@ const ACTIVITY_EVENTS = [
2929
// Throttle activity detection to avoid excessive timer resets
3030
const THROTTLE_MS = 1000;
3131

32+
// Grace period before locking on minimize/hide — prevents locking on brief
33+
// tab switches, notification clicks, or quick app toggles.
34+
const MINIMIZE_GRACE_MS = 30_000; // 30 seconds
35+
3236
let _timer = null;
3337
let _lastActivity = Date.now();
3438
let _onLock = null;
3539
let _paused = false;
3640
let _started = false;
3741
let _throttleTimeout = null;
3842
let _visibilityHandler = null;
43+
let _minimizeGraceTimer = null;
3944
let _tauriUnlisteners = [];
4045

4146
/**
@@ -87,11 +92,29 @@ function start(onLock) {
8792
document.addEventListener(event, onActivity, { passive: true, capture: true });
8893
}
8994

90-
// Visibility change handler (lock on minimize)
95+
// Visibility change handler (lock on minimize with grace period)
9196
_visibilityHandler = () => {
9297
const prefs = getLockPrefs();
93-
if (prefs.lockOnMinimize && document.hidden && _onLock && !_paused) {
94-
_onLock();
98+
if (!prefs.lockOnMinimize || _paused) return;
99+
100+
if (document.hidden) {
101+
// Start grace period — don't lock immediately so brief tab switches
102+
// (checking a notification, switching apps momentarily) don't trigger it
103+
if (!_minimizeGraceTimer && _onLock) {
104+
_minimizeGraceTimer = setTimeout(() => {
105+
_minimizeGraceTimer = null;
106+
// Re-check: still hidden and not paused?
107+
if (document.hidden && !_paused && _started && _onLock) {
108+
_onLock();
109+
}
110+
}, MINIMIZE_GRACE_MS);
111+
}
112+
} else {
113+
// User returned — cancel the grace timer
114+
if (_minimizeGraceTimer) {
115+
clearTimeout(_minimizeGraceTimer);
116+
_minimizeGraceTimer = null;
117+
}
95118
}
96119
};
97120
document.addEventListener('visibilitychange', _visibilityHandler);
@@ -112,12 +135,25 @@ async function setupTauriListeners() {
112135
const { getCurrentWindow } = await import('@tauri-apps/api/window');
113136
const appWindow = getCurrentWindow();
114137

115-
// Lock on window blur if lockOnMinimize is enabled
138+
// Lock on window blur if lockOnMinimize is enabled (with grace period)
116139
const unlistenBlur = await appWindow.onFocusChanged(({ payload: focused }) => {
117-
if (!focused && !_paused && _started) {
118-
const prefs = getLockPrefs();
119-
if (prefs.lockOnMinimize && _onLock) {
120-
_onLock();
140+
if (!_started || _paused) return;
141+
const prefs = getLockPrefs();
142+
if (!prefs.lockOnMinimize) return;
143+
144+
if (!focused) {
145+
if (!_minimizeGraceTimer && _onLock) {
146+
_minimizeGraceTimer = setTimeout(() => {
147+
_minimizeGraceTimer = null;
148+
if (!_paused && _started && _onLock) {
149+
_onLock();
150+
}
151+
}, MINIMIZE_GRACE_MS);
152+
}
153+
} else {
154+
if (_minimizeGraceTimer) {
155+
clearTimeout(_minimizeGraceTimer);
156+
_minimizeGraceTimer = null;
121157
}
122158
}
123159
});
@@ -144,6 +180,11 @@ function stop() {
144180
_throttleTimeout = null;
145181
}
146182

183+
if (_minimizeGraceTimer) {
184+
clearTimeout(_minimizeGraceTimer);
185+
_minimizeGraceTimer = null;
186+
}
187+
147188
for (const event of ACTIVITY_EVENTS) {
148189
document.removeEventListener(event, onActivity, { capture: true });
149190
}
@@ -175,6 +216,10 @@ function pause() {
175216
clearTimeout(_timer);
176217
_timer = null;
177218
}
219+
if (_minimizeGraceTimer) {
220+
clearTimeout(_minimizeGraceTimer);
221+
_minimizeGraceTimer = null;
222+
}
178223
}
179224

180225
/**

0 commit comments

Comments
 (0)