@bobfrankston/rmfmail 1.0.570 → 1.0.589
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/TODO.md +18 -3
- package/bin/build-rmfmailto-exe.js +83 -0
- package/bin/mailto.js +130 -0
- package/bin/mailto.js.map +1 -0
- package/bin/mailto.ts +122 -0
- package/bin/mailx.js +199 -3
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +188 -3
- package/bin/rmfmailto-src/Cargo.lock +92 -0
- package/bin/rmfmailto-src/Cargo.toml +20 -0
- package/bin/rmfmailto-src/build.rs +27 -0
- package/bin/rmfmailto-src/src/main.rs +143 -0
- package/bin/rmfmailto.exe +0 -0
- package/client/app.js +157 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +157 -0
- package/client/components/alarms.js +5 -2
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +4 -2
- package/client/components/folder-tree.js +43 -2
- package/client/components/folder-tree.js.map +1 -1
- package/client/components/folder-tree.ts +40 -2
- package/client/lib/api-client.js +7 -0
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +11 -0
- package/client/lib/mailxapi.js +3 -0
- package/client/styles/components.css +40 -0
- package/package.json +12 -12
- package/packages/mailx-imap/index.d.ts +46 -14
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +527 -187
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +518 -172
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-server/index.js +4 -4
- package/packages/mailx-server/index.js.map +1 -1
- package/packages/mailx-server/index.ts +4 -4
- package/packages/mailx-service/index.d.ts +14 -0
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +61 -8
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +59 -8
- package/packages/mailx-service/jsonrpc.js +2 -0
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +2 -0
- package/packages/mailx-service/reconciler.d.ts.map +1 -1
- package/packages/mailx-service/reconciler.js +16 -0
- package/packages/mailx-service/reconciler.js.map +1 -1
- package/packages/mailx-service/reconciler.ts +15 -0
- package/packages/mailx-store/db.js +2 -2
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +2 -2
- package/packages/mailx-store/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "proc-macro2"
|
|
7
|
+
version = "1.0.106"
|
|
8
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
9
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"unicode-ident",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "quote"
|
|
16
|
+
version = "1.0.45"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"proc-macro2",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "rmfmailto"
|
|
25
|
+
version = "1.0.0"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"winres",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[package]]
|
|
31
|
+
name = "serde"
|
|
32
|
+
version = "1.0.228"
|
|
33
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
34
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
35
|
+
dependencies = [
|
|
36
|
+
"serde_core",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[package]]
|
|
40
|
+
name = "serde_core"
|
|
41
|
+
version = "1.0.228"
|
|
42
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
43
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
44
|
+
dependencies = [
|
|
45
|
+
"serde_derive",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[[package]]
|
|
49
|
+
name = "serde_derive"
|
|
50
|
+
version = "1.0.228"
|
|
51
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
52
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
53
|
+
dependencies = [
|
|
54
|
+
"proc-macro2",
|
|
55
|
+
"quote",
|
|
56
|
+
"syn",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "syn"
|
|
61
|
+
version = "2.0.117"
|
|
62
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
63
|
+
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
|
64
|
+
dependencies = [
|
|
65
|
+
"proc-macro2",
|
|
66
|
+
"quote",
|
|
67
|
+
"unicode-ident",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[[package]]
|
|
71
|
+
name = "toml"
|
|
72
|
+
version = "0.5.11"
|
|
73
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
+
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
|
75
|
+
dependencies = [
|
|
76
|
+
"serde",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[[package]]
|
|
80
|
+
name = "unicode-ident"
|
|
81
|
+
version = "1.0.24"
|
|
82
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
83
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "winres"
|
|
87
|
+
version = "0.1.12"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
|
90
|
+
dependencies = [
|
|
91
|
+
"toml",
|
|
92
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "rmfmailto"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "rmfmail mailto: handler launcher"
|
|
6
|
+
authors = ["Bob Frankston"]
|
|
7
|
+
|
|
8
|
+
[[bin]]
|
|
9
|
+
name = "rmfmailto"
|
|
10
|
+
path = "src/main.rs"
|
|
11
|
+
|
|
12
|
+
[build-dependencies]
|
|
13
|
+
winres = "0.1"
|
|
14
|
+
|
|
15
|
+
[profile.release]
|
|
16
|
+
opt-level = "z"
|
|
17
|
+
lto = true
|
|
18
|
+
codegen-units = 1
|
|
19
|
+
panic = "abort"
|
|
20
|
+
strip = true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Embed Win32 VERSIONINFO so the Win11 "Select an app" picker shows the
|
|
2
|
+
// app as "rmfmail" instead of falling through to the host EXE's PE
|
|
3
|
+
// FileDescription. Keys here populate the picker's display.
|
|
4
|
+
|
|
5
|
+
#[cfg(windows)]
|
|
6
|
+
fn main() {
|
|
7
|
+
let mut res = winres::WindowsResource::new();
|
|
8
|
+
res.set("ProductName", "rmfmail");
|
|
9
|
+
res.set("FileDescription", "rmfmail");
|
|
10
|
+
res.set("CompanyName", "Bob Frankston");
|
|
11
|
+
res.set("LegalCopyright", "MIT");
|
|
12
|
+
res.set("OriginalFilename", "rmfmailto.exe");
|
|
13
|
+
res.set("InternalName", "rmfmailto");
|
|
14
|
+
// ICO can be picked up at registration time via DefaultIcon — embedding
|
|
15
|
+
// here gets it on the picker tile too. The path resolves relative to
|
|
16
|
+
// the crate root at build time.
|
|
17
|
+
let ico = std::path::Path::new("../../client/icon.ico");
|
|
18
|
+
if ico.exists() {
|
|
19
|
+
res.set_icon(ico.to_str().unwrap());
|
|
20
|
+
}
|
|
21
|
+
if let Err(e) = res.compile() {
|
|
22
|
+
eprintln!("winres compile failed: {e}");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[cfg(not(windows))]
|
|
27
|
+
fn main() {}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// rmfmailto — Win32 launcher for the OS-level mailto: handler (P115).
|
|
2
|
+
//
|
|
3
|
+
// Exists for ONE reason: Win11's "Select an app to open this mailto" picker
|
|
4
|
+
// reads the EXE's PE FileDescription as the display name. If we register
|
|
5
|
+
// `node.exe ... mailx.js --mailto "%1"` the picker shows "Node.js
|
|
6
|
+
// JavaScript Runtime" — confusing. With this binary registered instead,
|
|
7
|
+
// the picker shows "rmfmail" (set in build.rs's VERSIONINFO).
|
|
8
|
+
//
|
|
9
|
+
// The runtime job is trivial: receive argv (the mailto URL), find node + the
|
|
10
|
+
// installed mailx.js, spawn `node mailx.js --mailto <url>`, exit. Detached
|
|
11
|
+
// so we don't hold a handle on a console window.
|
|
12
|
+
//
|
|
13
|
+
// Lookup order for node + mailx.js:
|
|
14
|
+
// 1. NODE_EXE / RMFMAIL_JS env vars (test override / unusual installs).
|
|
15
|
+
// 2. %APPDATA%\npm\node_modules\@bobfrankston\rmfmail\bin\mailx.js
|
|
16
|
+
// (standard `npm install -g @bobfrankston/rmfmail` location).
|
|
17
|
+
// 3. PATH lookup for `node.exe`.
|
|
18
|
+
// Failing any of those, surface a single MessageBox so the user gets a
|
|
19
|
+
// readable error instead of a silent dead click.
|
|
20
|
+
|
|
21
|
+
#![windows_subsystem = "windows"]
|
|
22
|
+
|
|
23
|
+
use std::env;
|
|
24
|
+
use std::path::PathBuf;
|
|
25
|
+
use std::process::Command;
|
|
26
|
+
|
|
27
|
+
#[cfg(windows)]
|
|
28
|
+
fn show_error(msg: &str) {
|
|
29
|
+
use std::ffi::OsStr;
|
|
30
|
+
use std::iter::once;
|
|
31
|
+
use std::os::windows::ffi::OsStrExt;
|
|
32
|
+
#[link(name = "user32")]
|
|
33
|
+
extern "system" {
|
|
34
|
+
fn MessageBoxW(hwnd: usize, text: *const u16, caption: *const u16, ty: u32) -> i32;
|
|
35
|
+
}
|
|
36
|
+
let to_wide = |s: &str| -> Vec<u16> {
|
|
37
|
+
OsStr::new(s).encode_wide().chain(once(0)).collect()
|
|
38
|
+
};
|
|
39
|
+
let text = to_wide(msg);
|
|
40
|
+
let caption = to_wide("rmfmail mailto handler");
|
|
41
|
+
unsafe { MessageBoxW(0, text.as_ptr(), caption.as_ptr(), 0x10); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[cfg(not(windows))]
|
|
45
|
+
fn show_error(msg: &str) { eprintln!("{msg}"); }
|
|
46
|
+
|
|
47
|
+
fn find_node() -> Option<PathBuf> {
|
|
48
|
+
if let Ok(p) = env::var("NODE_EXE") {
|
|
49
|
+
let pb = PathBuf::from(p);
|
|
50
|
+
if pb.is_file() { return Some(pb); }
|
|
51
|
+
}
|
|
52
|
+
// Look up node on PATH. Windows's where.exe finds it but isn't always
|
|
53
|
+
// available; instead walk PATH manually.
|
|
54
|
+
let path = env::var_os("PATH")?;
|
|
55
|
+
for dir in env::split_paths(&path) {
|
|
56
|
+
for name in &["node.exe", "node"] {
|
|
57
|
+
let candidate = dir.join(name);
|
|
58
|
+
if candidate.is_file() { return Some(candidate); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
None
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn find_mailx_js() -> Option<PathBuf> {
|
|
65
|
+
if let Ok(p) = env::var("RMFMAIL_JS") {
|
|
66
|
+
let pb = PathBuf::from(p);
|
|
67
|
+
if pb.is_file() { return Some(pb); }
|
|
68
|
+
}
|
|
69
|
+
// Standard global-install location.
|
|
70
|
+
if let Ok(appdata) = env::var("APPDATA") {
|
|
71
|
+
let pb = PathBuf::from(appdata)
|
|
72
|
+
.join("npm").join("node_modules")
|
|
73
|
+
.join("@bobfrankston").join("rmfmail")
|
|
74
|
+
.join("bin").join("mailx.js");
|
|
75
|
+
if pb.is_file() { return Some(pb); }
|
|
76
|
+
}
|
|
77
|
+
// Fallback: relative to this binary, walking up to find `bin/mailx.js`.
|
|
78
|
+
// Lets a developer-mode build (rmfmailto.exe sitting next to mailx.js
|
|
79
|
+
// in `app/bin/`) work without env-var fiddling.
|
|
80
|
+
if let Ok(exe) = env::current_exe() {
|
|
81
|
+
let mut p = exe.clone();
|
|
82
|
+
for _ in 0..5 {
|
|
83
|
+
if let Some(parent) = p.parent() {
|
|
84
|
+
let candidate = parent.join("mailx.js");
|
|
85
|
+
if candidate.is_file() { return Some(candidate); }
|
|
86
|
+
let candidate = parent.join("bin").join("mailx.js");
|
|
87
|
+
if candidate.is_file() { return Some(candidate); }
|
|
88
|
+
p = parent.to_path_buf();
|
|
89
|
+
} else {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
None
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn main() {
|
|
98
|
+
// argv[0] is the exe path itself; argv[1] should be the mailto URL.
|
|
99
|
+
let mailto_url: Option<String> = env::args().nth(1);
|
|
100
|
+
let mailto_url = match mailto_url {
|
|
101
|
+
Some(u) if !u.is_empty() => u,
|
|
102
|
+
_ => {
|
|
103
|
+
show_error("rmfmailto invoked without a mailto URL.\n\nUsage: rmfmailto.exe <mailto:...>");
|
|
104
|
+
std::process::exit(1);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let node = match find_node() {
|
|
109
|
+
Some(p) => p,
|
|
110
|
+
None => {
|
|
111
|
+
show_error("Node.js was not found on PATH.\n\nrmfmail requires Node.js to be installed. See https://nodejs.org");
|
|
112
|
+
std::process::exit(2);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
let mailx_js = match find_mailx_js() {
|
|
116
|
+
Some(p) => p,
|
|
117
|
+
None => {
|
|
118
|
+
show_error("rmfmail's mailx.js was not located.\n\nExpected at:\n %APPDATA%\\npm\\node_modules\\@bobfrankston\\rmfmail\\bin\\mailx.js\n\nSet RMFMAIL_JS to override.");
|
|
119
|
+
std::process::exit(3);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Spawn detached so the picker doesn't get a "is this still running?"
|
|
124
|
+
// console window. CREATE_NO_WINDOW (0x08000000) + DETACHED_PROCESS
|
|
125
|
+
// (0x00000008) on Windows; nothing special needed elsewhere.
|
|
126
|
+
let mut cmd = Command::new(&node);
|
|
127
|
+
cmd.arg(&mailx_js).arg("--mailto").arg(&mailto_url);
|
|
128
|
+
#[cfg(windows)]
|
|
129
|
+
{
|
|
130
|
+
use std::os::windows::process::CommandExt;
|
|
131
|
+
cmd.creation_flags(0x08000000 | 0x00000008);
|
|
132
|
+
}
|
|
133
|
+
match cmd.spawn() {
|
|
134
|
+
Ok(_) => {}
|
|
135
|
+
Err(e) => {
|
|
136
|
+
show_error(&format!(
|
|
137
|
+
"Failed to launch rmfmail.\n\nNode: {}\nScript: {}\nError: {e}",
|
|
138
|
+
node.display(), mailx_js.display(),
|
|
139
|
+
));
|
|
140
|
+
std::process::exit(4);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
Binary file
|
package/client/app.js
CHANGED
|
@@ -2294,8 +2294,51 @@ onWsEvent((event) => {
|
|
|
2294
2294
|
}
|
|
2295
2295
|
break;
|
|
2296
2296
|
}
|
|
2297
|
+
case "openMailto": {
|
|
2298
|
+
// P115: OS-level mailto: handler. CLI dropped a pending file; the
|
|
2299
|
+
// daemon picked it up and forwarded the parsed fields. Open compose
|
|
2300
|
+
// pre-populated. Treat as a fresh New compose, not a reply — the
|
|
2301
|
+
// browser doesn't know about a parent message.
|
|
2302
|
+
openComposeFromMailto({
|
|
2303
|
+
to: Array.isArray(event.to) ? event.to : [],
|
|
2304
|
+
cc: Array.isArray(event.cc) ? event.cc : [],
|
|
2305
|
+
bcc: Array.isArray(event.bcc) ? event.bcc : [],
|
|
2306
|
+
subject: event.subject || "",
|
|
2307
|
+
body: event.body || "",
|
|
2308
|
+
inReplyTo: event.inReplyTo || "",
|
|
2309
|
+
}).catch((e) => console.error("openComposeFromMailto failed:", e?.message || e));
|
|
2310
|
+
break;
|
|
2311
|
+
}
|
|
2297
2312
|
}
|
|
2298
2313
|
});
|
|
2314
|
+
/** Open a compose window pre-filled from a mailto: URL. Same plumbing as
|
|
2315
|
+
* openCompose("new") but the init shape comes from the parsed URL instead
|
|
2316
|
+
* of the currently-selected message. The body arrives as plain text from
|
|
2317
|
+
* the URL — we wrap it in a single <p> so Quill renders it as paragraphs
|
|
2318
|
+
* rather than a single line, escaping `<` so embedded angle brackets in a
|
|
2319
|
+
* signature/template don't get interpreted as tags. */
|
|
2320
|
+
async function openComposeFromMailto(m) {
|
|
2321
|
+
const accounts = await getAccounts();
|
|
2322
|
+
const accountId = accounts[0]?.id || "";
|
|
2323
|
+
const escape = (s) => s.replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
|
2324
|
+
const bodyHtml = m.body
|
|
2325
|
+
? "<p>" + escape(m.body).replace(/\r?\n/g, "</p><p>") + "</p>"
|
|
2326
|
+
: "";
|
|
2327
|
+
const init = {
|
|
2328
|
+
mode: "new",
|
|
2329
|
+
accountId,
|
|
2330
|
+
to: m.to.map(addr => ({ name: "", address: addr })),
|
|
2331
|
+
cc: m.cc.map(addr => ({ name: "", address: addr })),
|
|
2332
|
+
bcc: m.bcc.map(addr => ({ name: "", address: addr })),
|
|
2333
|
+
subject: m.subject,
|
|
2334
|
+
bodyHtml,
|
|
2335
|
+
inReplyTo: m.inReplyTo,
|
|
2336
|
+
references: m.inReplyTo ? [m.inReplyTo] : [],
|
|
2337
|
+
accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
|
|
2338
|
+
};
|
|
2339
|
+
sessionStorage.setItem("composeInit", JSON.stringify(init));
|
|
2340
|
+
showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
|
|
2341
|
+
}
|
|
2299
2342
|
// ── Keyboard shortcuts ──
|
|
2300
2343
|
// Capture-phase pre-handler: intercept WebView accelerator keys that would
|
|
2301
2344
|
// otherwise trigger a browser action (Ctrl+R / Ctrl+Shift+R = reload). Call
|
|
@@ -2797,6 +2840,120 @@ function applyThreadFilter() {
|
|
|
2797
2840
|
console.error("alarm poller init failed:", e?.message || e);
|
|
2798
2841
|
}
|
|
2799
2842
|
})();
|
|
2843
|
+
// Pull-to-refresh: touch-drag down at the top of the message list fires a
|
|
2844
|
+
// sync, same as F5 / btn-sync. Standard Android / Thunderbird / mobile
|
|
2845
|
+
// pattern. The indicator slides down with the drag and rotates the chevron
|
|
2846
|
+
// once the user crosses the trigger threshold; releasing past the threshold
|
|
2847
|
+
// fires triggerSync(). Below threshold = snap back without action.
|
|
2848
|
+
//
|
|
2849
|
+
// Touch-only by design — desktop has the F5 / sync button. Pointer events
|
|
2850
|
+
// would also catch mouse-drag, which is more annoying than useful (people
|
|
2851
|
+
// drag the scrollbar). The `e.touches.length === 1` check skips two-finger
|
|
2852
|
+
// gestures (pinch-zoom, two-finger scroll).
|
|
2853
|
+
(() => {
|
|
2854
|
+
const PTR_THRESHOLD_PX = 80; // pull this far to arm the trigger
|
|
2855
|
+
const PTR_MAX_PX = 120; // cap the visual translate
|
|
2856
|
+
const mlBody = document.getElementById("ml-body");
|
|
2857
|
+
if (!mlBody)
|
|
2858
|
+
return;
|
|
2859
|
+
const indicator = document.createElement("div");
|
|
2860
|
+
indicator.className = "ptr-indicator";
|
|
2861
|
+
indicator.innerHTML = `<span class="ptr-chev"></span><span class="ptr-label">Pull to refresh</span>`;
|
|
2862
|
+
mlBody.parentElement?.insertBefore(indicator, mlBody);
|
|
2863
|
+
const labelEl = indicator.querySelector(".ptr-label");
|
|
2864
|
+
let startY = 0;
|
|
2865
|
+
let pullY = 0;
|
|
2866
|
+
let active = false;
|
|
2867
|
+
let refreshing = false;
|
|
2868
|
+
const setPull = (px, armed) => {
|
|
2869
|
+
const clamped = Math.min(PTR_MAX_PX, Math.max(0, px));
|
|
2870
|
+
indicator.classList.toggle("armed", armed);
|
|
2871
|
+
indicator.style.transform = `translateY(calc(-100% + ${clamped}px))`;
|
|
2872
|
+
};
|
|
2873
|
+
const reset = () => {
|
|
2874
|
+
indicator.classList.remove("dragging", "armed");
|
|
2875
|
+
indicator.style.transform = "";
|
|
2876
|
+
};
|
|
2877
|
+
mlBody.addEventListener("touchstart", (e) => {
|
|
2878
|
+
if (refreshing)
|
|
2879
|
+
return;
|
|
2880
|
+
if (e.touches.length !== 1)
|
|
2881
|
+
return;
|
|
2882
|
+
if (mlBody.scrollTop > 0)
|
|
2883
|
+
return;
|
|
2884
|
+
startY = e.touches[0].clientY;
|
|
2885
|
+
pullY = 0;
|
|
2886
|
+
active = true;
|
|
2887
|
+
indicator.classList.add("dragging");
|
|
2888
|
+
labelEl.textContent = "Pull to refresh";
|
|
2889
|
+
}, { passive: true });
|
|
2890
|
+
mlBody.addEventListener("touchmove", (e) => {
|
|
2891
|
+
if (!active || refreshing)
|
|
2892
|
+
return;
|
|
2893
|
+
const dy = e.touches[0].clientY - startY;
|
|
2894
|
+
if (dy <= 0) {
|
|
2895
|
+
// User scrolled up past the start — abandon, let normal scroll
|
|
2896
|
+
// take over without reverting (the touchend handler will reset).
|
|
2897
|
+
pullY = 0;
|
|
2898
|
+
setPull(0, false);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
// Resist past 0 — pull "stretches" rather than scrolls. preventDefault
|
|
2902
|
+
// is necessary to keep the browser from interpreting the drag as
|
|
2903
|
+
// overscroll / browser-level pull-to-refresh.
|
|
2904
|
+
if (e.cancelable)
|
|
2905
|
+
e.preventDefault();
|
|
2906
|
+
pullY = dy * 0.5; // 0.5 = rubber-band damping
|
|
2907
|
+
const armed = pullY >= PTR_THRESHOLD_PX;
|
|
2908
|
+
labelEl.textContent = armed ? "Release to refresh" : "Pull to refresh";
|
|
2909
|
+
setPull(pullY, armed);
|
|
2910
|
+
}, { passive: false });
|
|
2911
|
+
const finish = async () => {
|
|
2912
|
+
if (!active)
|
|
2913
|
+
return;
|
|
2914
|
+
active = false;
|
|
2915
|
+
if (pullY < PTR_THRESHOLD_PX) {
|
|
2916
|
+
reset();
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
// Armed — show spinner, fire sync, clear when complete.
|
|
2920
|
+
refreshing = true;
|
|
2921
|
+
indicator.classList.remove("armed", "dragging");
|
|
2922
|
+
indicator.classList.add("refreshing");
|
|
2923
|
+
indicator.style.transform = `translateY(calc(-100% + 60px))`;
|
|
2924
|
+
labelEl.textContent = "Refreshing...";
|
|
2925
|
+
try {
|
|
2926
|
+
const { triggerSync } = await import("./lib/api-client.js");
|
|
2927
|
+
await triggerSync();
|
|
2928
|
+
}
|
|
2929
|
+
catch (e) {
|
|
2930
|
+
console.error("pull-to-refresh failed:", e?.message || e);
|
|
2931
|
+
}
|
|
2932
|
+
refreshing = false;
|
|
2933
|
+
indicator.classList.remove("refreshing");
|
|
2934
|
+
reset();
|
|
2935
|
+
};
|
|
2936
|
+
mlBody.addEventListener("touchend", () => { finish(); }, { passive: true });
|
|
2937
|
+
mlBody.addEventListener("touchcancel", () => { active = false; reset(); }, { passive: true });
|
|
2938
|
+
})();
|
|
2939
|
+
// P115: pick up a pending mailto: drop file dropped by the CLI invocation
|
|
2940
|
+
// that spawned us (or by an earlier --mailto run that the user hasn't yet
|
|
2941
|
+
// acknowledged). The IPC call deletes the file as part of reading it, so a
|
|
2942
|
+
// race with the daemon's fs.watch is harmless — whichever fires first wins
|
|
2943
|
+
// and the other gets null. Subsequent live clicks during this session
|
|
2944
|
+
// arrive via the `openMailto` event handled in onEvent above.
|
|
2945
|
+
(async () => {
|
|
2946
|
+
try {
|
|
2947
|
+
const { consumePendingMailto } = await import("./lib/api-client.js");
|
|
2948
|
+
const data = await consumePendingMailto();
|
|
2949
|
+
if (data && (data.to.length || data.subject || data.body)) {
|
|
2950
|
+
await openComposeFromMailto(data);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
catch (e) {
|
|
2954
|
+
console.error("pending mailto pickup failed:", e?.message || e);
|
|
2955
|
+
}
|
|
2956
|
+
})();
|
|
2800
2957
|
// Two-line toggle
|
|
2801
2958
|
optTwoLine?.addEventListener("change", () => {
|
|
2802
2959
|
const list = document.getElementById("message-list");
|