relay-companion 0.1.7 → 0.1.8
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/overlay/inbox.html +134 -3
- package/overlay/main.cjs +92 -3
- package/overlay/preload.cjs +7 -1
- package/overlay/relayTrayTemplate.png +0 -0
- package/overlay/relayTrayTemplate@2x.png +0 -0
- package/package.json +1 -1
package/overlay/inbox.html
CHANGED
|
@@ -57,6 +57,18 @@
|
|
|
57
57
|
}
|
|
58
58
|
@keyframes appIn { from { opacity:0; transform:scale(.97); } to { opacity:1; transform:none; } }
|
|
59
59
|
|
|
60
|
+
/* Dismissal states. `offstage` parks the card invisibly while the OS window is hidden.
|
|
61
|
+
`bye` is the graceful exit (✕ click, or a ghost notification finishing). A `ghost`
|
|
62
|
+
card is the designed notification shown while the pill is dismissed: it emerges like
|
|
63
|
+
a banner (drift down + fade) and NEVER folds back into a pill — it just leaves. */
|
|
64
|
+
.card.offstage { visibility:hidden; animation:none; }
|
|
65
|
+
.card.bye { animation:byeOut .3s var(--settle) both; pointer-events:none; }
|
|
66
|
+
@keyframes byeOut { from { opacity:1; transform:none; } to { opacity:0; transform:scale(.96) translateY(-10px); } }
|
|
67
|
+
.card.ghost { animation:ghostIn .5s var(--soft) both; }
|
|
68
|
+
@keyframes ghostIn { from { opacity:0; transform:translateY(-14px) scale(.98); } to { opacity:1; transform:none; } }
|
|
69
|
+
.card.ghost.bye { animation:ghostOut .38s var(--settle) both; }
|
|
70
|
+
@keyframes ghostOut { from { opacity:1; transform:none; } to { opacity:0; transform:translateY(-12px) scale(.985); } }
|
|
71
|
+
|
|
60
72
|
/* brand lockup — present in BOTH states; no surface, no rule */
|
|
61
73
|
.lockup {
|
|
62
74
|
flex:0 0 44px; display:flex; align-items:center; gap:9px;
|
|
@@ -78,7 +90,9 @@
|
|
|
78
90
|
transition:color .18s var(--settle), opacity .18s var(--settle), width .18s var(--settle), margin .18s var(--settle);
|
|
79
91
|
}
|
|
80
92
|
.lockup:hover .minimize-hint { color:var(--muted-2); }
|
|
81
|
-
.card.collapsed .minimize-hint
|
|
93
|
+
.card.collapsed .minimize-hint,
|
|
94
|
+
.card.peek .minimize-hint {
|
|
95
|
+
/* folded pill and notification banner: tapping the lockup EXPANDS, so no "Minimize" */
|
|
82
96
|
width:0;
|
|
83
97
|
margin-right:0;
|
|
84
98
|
overflow:hidden;
|
|
@@ -89,6 +103,17 @@
|
|
|
89
103
|
color:var(--accent); line-height:1; transition:color .3s var(--settle);
|
|
90
104
|
}
|
|
91
105
|
.count.zero { color:var(--muted-3); }
|
|
106
|
+
/* ✕ — fully hide the overlay; the Relay mark in the menu bar brings it back. Present in
|
|
107
|
+
every card state (expanded, pill, notification) so the pill is always escapable. */
|
|
108
|
+
.close-x {
|
|
109
|
+
flex:0 0 auto; appearance:none; border:0; background:transparent; cursor:pointer;
|
|
110
|
+
width:20px; height:20px; border-radius:6px; padding:0; margin-left:6px; margin-right:-6px;
|
|
111
|
+
display:inline-flex; align-items:center; justify-content:center;
|
|
112
|
+
color:var(--muted-3);
|
|
113
|
+
transition:color .15s var(--settle), background-color .15s var(--settle);
|
|
114
|
+
}
|
|
115
|
+
.close-x:hover { color:var(--ink-2); background:rgba(31,26,23,.06); }
|
|
116
|
+
.close-x:active { color:var(--ink); }
|
|
92
117
|
@keyframes countRoll { from { transform:translateY(-6px); opacity:.3; } to { transform:none; opacity:1; } }
|
|
93
118
|
|
|
94
119
|
/* tab control — individual rounded-rect buttons (the loved control) */
|
|
@@ -412,6 +437,9 @@
|
|
|
412
437
|
<span class="spacer"></span>
|
|
413
438
|
<span class="minimize-hint">Minimize</span>
|
|
414
439
|
<span class="count zero" id="count">0</span>
|
|
440
|
+
<button class="close-x" id="closeX" type="button" aria-label="Hide Relay" title="Hide Relay — reopen from the menu bar">
|
|
441
|
+
<svg width="9" height="9" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M1 1l8 8M9 1L1 9"/></svg>
|
|
442
|
+
</button>
|
|
415
443
|
</div>
|
|
416
444
|
|
|
417
445
|
<nav class="tabs" aria-label="Relay views">
|
|
@@ -484,6 +512,7 @@
|
|
|
484
512
|
<script>
|
|
485
513
|
const cardEl = document.getElementById("card");
|
|
486
514
|
const lockupEl = document.getElementById("lockup");
|
|
515
|
+
const closeXEl = document.getElementById("closeX");
|
|
487
516
|
const countEl = document.getElementById("count");
|
|
488
517
|
const tabEls = [...document.querySelectorAll(".tab")];
|
|
489
518
|
const scrollEl = document.getElementById("scroll");
|
|
@@ -512,7 +541,7 @@
|
|
|
512
541
|
|
|
513
542
|
const REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
514
543
|
const EXPANDED = { w: 344, h: 524 };
|
|
515
|
-
const PILL = { w:
|
|
544
|
+
const PILL = { w: 178, h: 44 }; // wide enough for mark + word + count + ✕
|
|
516
545
|
const PEEK = { w: 344, h: 118 };
|
|
517
546
|
|
|
518
547
|
// ---------- zero-latency sound (Web Audio, pre-decoded) ----------
|
|
@@ -581,6 +610,7 @@
|
|
|
581
610
|
// ---------- collapse / expand / peek ----------
|
|
582
611
|
let collapsed = false;
|
|
583
612
|
let peeking = false, peekTimer = null;
|
|
613
|
+
let ghost = false, ghostTimer = null; // notification-only mode while the pill is dismissed
|
|
584
614
|
function clearPeek() {
|
|
585
615
|
if (peekTimer) { clearTimeout(peekTimer); peekTimer = null; }
|
|
586
616
|
peeking = false;
|
|
@@ -594,6 +624,87 @@
|
|
|
594
624
|
springTo(collapsed ? PILL.w : EXPANDED.w, collapsed ? PILL.h : EXPANDED.h);
|
|
595
625
|
playTink();
|
|
596
626
|
}
|
|
627
|
+
// Park the card invisibly at expanded size while the OS window is hidden, so the
|
|
628
|
+
// next appearance (tray reopen / ghost banner) starts from a clean slate.
|
|
629
|
+
function snapSize(w, h) {
|
|
630
|
+
W.v = W.t = w; H.v = H.t = h;
|
|
631
|
+
if (raf) { cancelAnimationFrame(raf); raf = null; cardEl.style.willChange = ""; }
|
|
632
|
+
cardEl.style.width = w + "px";
|
|
633
|
+
cardEl.style.height = h + "px";
|
|
634
|
+
}
|
|
635
|
+
function resetOffstage() {
|
|
636
|
+
ghost = false;
|
|
637
|
+
if (ghostTimer) { clearTimeout(ghostTimer); ghostTimer = null; }
|
|
638
|
+
clearPeek();
|
|
639
|
+
collapsed = false;
|
|
640
|
+
cardEl.classList.add("offstage");
|
|
641
|
+
cardEl.classList.remove("collapsed", "ghost", "bye");
|
|
642
|
+
snapSize(EXPANDED.w, EXPANDED.h);
|
|
643
|
+
}
|
|
644
|
+
// ✕ — the card leaves gracefully, then the main process hides the window and keeps
|
|
645
|
+
// it hidden (persisted) until the menu-bar Relay mark is clicked.
|
|
646
|
+
function dismissOverlay() {
|
|
647
|
+
if (cardEl.classList.contains("bye")) return;
|
|
648
|
+
if (ghostTimer) { clearTimeout(ghostTimer); ghostTimer = null; }
|
|
649
|
+
if (peekTimer) { clearTimeout(peekTimer); peekTimer = null; }
|
|
650
|
+
const wasGhost = ghost;
|
|
651
|
+
cardEl.classList.add("bye");
|
|
652
|
+
setInteractive(false);
|
|
653
|
+
setTimeout(() => {
|
|
654
|
+
if (wasGhost) window.relay.notifDone(); // already dismissed; just re-hide the window
|
|
655
|
+
else window.relay.dismiss();
|
|
656
|
+
resetOffstage();
|
|
657
|
+
}, wasGhost ? 380 : 300);
|
|
658
|
+
}
|
|
659
|
+
// A relay arrived while dismissed: the same designed banner, but as a free-standing
|
|
660
|
+
// notification. It emerges at banner size (no pill morph — there is no pill), and
|
|
661
|
+
// when done it fades away instead of folding back into anything.
|
|
662
|
+
function ghostArrival(row) {
|
|
663
|
+
if (ghostTimer) { clearTimeout(ghostTimer); ghostTimer = null; }
|
|
664
|
+
clearPeek();
|
|
665
|
+
collapsed = false;
|
|
666
|
+
cardEl.classList.remove("collapsed", "offstage", "bye");
|
|
667
|
+
snapSize(PEEK.w, PEEK.h);
|
|
668
|
+
ghost = true; peeking = true;
|
|
669
|
+
cardEl.classList.add("peek", "ghost");
|
|
670
|
+
const kind = row && row.relayNotificationKind;
|
|
671
|
+
activeView = (kind === "task_request" || kind === "share_approval") ? "tasks" : "relays";
|
|
672
|
+
syncTabs();
|
|
673
|
+
applyView();
|
|
674
|
+
renderAll();
|
|
675
|
+
scrollEl.scrollTop = 0;
|
|
676
|
+
playTink();
|
|
677
|
+
// Leave after ~7s — but never rug-pull someone mid-reply or with the pointer on
|
|
678
|
+
// the banner; check again shortly instead (macOS banners hold while hovered too).
|
|
679
|
+
ghostTimer = setTimeout(function tick() {
|
|
680
|
+
ghostTimer = null;
|
|
681
|
+
if (interactive || quickReplyIsActive()) { ghostTimer = setTimeout(tick, 4000); return; }
|
|
682
|
+
dismissOverlay(); // rides the ghostOut exit, then main re-hides the window
|
|
683
|
+
}, 7000);
|
|
684
|
+
}
|
|
685
|
+
// Menu-bar Relay mark clicked: the card comes back FULLY OPEN (never the pill).
|
|
686
|
+
function trayOpen() {
|
|
687
|
+
const wasOffstage = cardEl.classList.contains("offstage") || cardEl.classList.contains("bye");
|
|
688
|
+
if (ghostTimer) { clearTimeout(ghostTimer); ghostTimer = null; }
|
|
689
|
+
ghost = false;
|
|
690
|
+
clearPeek();
|
|
691
|
+
collapsed = false;
|
|
692
|
+
cardEl.classList.remove("collapsed");
|
|
693
|
+
if (wasOffstage) {
|
|
694
|
+
// fresh entrance: snap to expanded and replay the appear animation
|
|
695
|
+
cardEl.classList.remove("ghost", "bye", "offstage");
|
|
696
|
+
snapSize(EXPANDED.w, EXPANDED.h);
|
|
697
|
+
cardEl.style.animation = "none";
|
|
698
|
+
void cardEl.offsetWidth;
|
|
699
|
+
cardEl.style.animation = "";
|
|
700
|
+
} else {
|
|
701
|
+
// visible (maybe mid-banner): keep animation-bearing classes untouched so nothing
|
|
702
|
+
// restarts — just morph to full size on the spring.
|
|
703
|
+
springTo(EXPANDED.w, EXPANDED.h);
|
|
704
|
+
}
|
|
705
|
+
renderAll();
|
|
706
|
+
playTink();
|
|
707
|
+
}
|
|
597
708
|
// A new relay arrived. If folded, peek open like an iOS banner (~7s) then fold back.
|
|
598
709
|
// The peek routes to the tab the item lives in: task_request / share_approval -> Tasks
|
|
599
710
|
// (the clean split), everything else -> Relays.
|
|
@@ -620,6 +731,14 @@
|
|
|
620
731
|
}, 7000);
|
|
621
732
|
}
|
|
622
733
|
function openFull() {
|
|
734
|
+
if (ghost) {
|
|
735
|
+
// Engaging with a ghost banner brings the pill back for real. Keep the .ghost
|
|
736
|
+
// class (removing it while visible would restart the base entrance animation);
|
|
737
|
+
// it only influences which exit animation would play later.
|
|
738
|
+
ghost = false;
|
|
739
|
+
if (ghostTimer) { clearTimeout(ghostTimer); ghostTimer = null; }
|
|
740
|
+
window.relay.undismiss();
|
|
741
|
+
}
|
|
623
742
|
clearPeek();
|
|
624
743
|
collapsed = false;
|
|
625
744
|
cardEl.classList.remove("collapsed");
|
|
@@ -627,6 +746,14 @@
|
|
|
627
746
|
playTink();
|
|
628
747
|
}
|
|
629
748
|
|
|
749
|
+
// ---------- the ✕ ----------
|
|
750
|
+
// Swallow the mousedown so the lockup's drag/tap-fold never engages.
|
|
751
|
+
closeXEl.addEventListener("mousedown", (e) => { e.stopPropagation(); });
|
|
752
|
+
closeXEl.addEventListener("click", (e) => {
|
|
753
|
+
e.stopPropagation();
|
|
754
|
+
dismissOverlay();
|
|
755
|
+
});
|
|
756
|
+
|
|
630
757
|
// ---------- drag the lockup to move, tap it to fold ----------
|
|
631
758
|
let dragging = false, offX = 0, offY = 0, downSX = 0, downSY = 0, moved = 0;
|
|
632
759
|
lockupEl.addEventListener("mousedown", (e) => {
|
|
@@ -1418,7 +1545,11 @@
|
|
|
1418
1545
|
}
|
|
1419
1546
|
|
|
1420
1547
|
window.relay.onInbox(onPayload);
|
|
1421
|
-
if (window.relay.onNewRelay) window.relay.onNewRelay((row) =>
|
|
1548
|
+
if (window.relay.onNewRelay) window.relay.onNewRelay((row, opts) => {
|
|
1549
|
+
if (opts && opts.ghost) ghostArrival(row); // pill dismissed -> free-standing banner
|
|
1550
|
+
else notifyArrival(row);
|
|
1551
|
+
});
|
|
1552
|
+
if (window.relay.onOpenFull) window.relay.onOpenFull(() => trayOpen());
|
|
1422
1553
|
if (window.relay.onOpenDone) window.relay.onOpenDone((id) => stopOpening(id));
|
|
1423
1554
|
window.relay.refresh().then((p) => {
|
|
1424
1555
|
onPayload(p);
|
package/overlay/main.cjs
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// on the card inside it, so it stays perfectly smooth. The empty area around the card is
|
|
7
7
|
// click-through, so the overlay never blocks the app behind it.
|
|
8
8
|
//
|
|
9
|
+
// Visibility: the card carries a ✕ that hides the overlay entirely (persisted). The Relay
|
|
10
|
+
// mark in the macOS menu bar (Tray) brings it back fully open. While dismissed, fresh
|
|
11
|
+
// relays still show the designed banner as a "ghost" notification — the window appears
|
|
12
|
+
// just for the banner, which fades away instead of folding back into a pill.
|
|
13
|
+
//
|
|
9
14
|
// Data:
|
|
10
15
|
// - Relay attention rows come from ${RELAY_HOME:-~/.relay-companion}/state.json under
|
|
11
16
|
// `state.packets` (staged by the companion daemon). Read it directly + fs.watch it.
|
|
@@ -14,7 +19,7 @@
|
|
|
14
19
|
// - Mutations (accept/reject/approve/decline/answer) call RelayClient with device-token
|
|
15
20
|
// auth. ack/mark-read writes state.json directly (atomic temp+rename).
|
|
16
21
|
|
|
17
|
-
const { app, BrowserWindow, ipcMain, shell, screen } = require("electron");
|
|
22
|
+
const { app, BrowserWindow, Tray, ipcMain, nativeImage, shell, screen } = require("electron");
|
|
18
23
|
const fs = require("node:fs");
|
|
19
24
|
const os = require("node:os");
|
|
20
25
|
const path = require("node:path");
|
|
@@ -36,11 +41,33 @@ const MARGIN = 8;
|
|
|
36
41
|
const DEFAULT_WEB_BASE = "https://sendrelays.com";
|
|
37
42
|
|
|
38
43
|
let win = null;
|
|
44
|
+
let tray = null;
|
|
39
45
|
let hostRunning = false; // Claude or Codex desktop app is running
|
|
40
46
|
let lastHost = null; // most-recently foregrounded host: "claude" | "codex"
|
|
41
47
|
let lastHostSeenAt = 0;
|
|
42
48
|
let lastSig = "";
|
|
43
49
|
|
|
50
|
+
// Dismissed mode: the user clicked the ✕ on the card, so the overlay stays hidden
|
|
51
|
+
// until they click the Relay menu-bar icon. Persisted across relaunches. While
|
|
52
|
+
// dismissed, a fresh relay still shows the designed notification (a "ghost" peek):
|
|
53
|
+
// the window appears just for the banner, then hides again when it finishes.
|
|
54
|
+
const OVERLAY_PREFS_PATH = path.join(RELAY_HOME, "overlay-prefs.json");
|
|
55
|
+
let dismissed = false;
|
|
56
|
+
let ghostActive = false; // a ghost notification is on screen while dismissed
|
|
57
|
+
let trayForcedVisible = false; // tray click while no host is running still shows the card
|
|
58
|
+
try {
|
|
59
|
+
dismissed = JSON.parse(fs.readFileSync(OVERLAY_PREFS_PATH, "utf8")).dismissed === true;
|
|
60
|
+
} catch {}
|
|
61
|
+
function setDismissed(value) {
|
|
62
|
+
dismissed = Boolean(value);
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(RELAY_HOME, { recursive: true });
|
|
65
|
+
fs.writeFileSync(OVERLAY_PREFS_PATH, `${JSON.stringify({ dismissed }, null, 2)}\n`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("[overlay] prefs write failed:", error && error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
// The ESM companion modules (config/client) are loaded lazily via dynamic import,
|
|
45
72
|
// because this overlay runs as CommonJS under Electron while the package is ESM.
|
|
46
73
|
let relayModules = null;
|
|
@@ -290,7 +317,20 @@ async function pushInbox(force) {
|
|
|
290
317
|
} else {
|
|
291
318
|
const fresh = rows.filter((r) => r.unread && !knownIds.has(r.id));
|
|
292
319
|
knownIds = new Set(rows.map((r) => r.id));
|
|
293
|
-
if (fresh.length && win && !win.isDestroyed())
|
|
320
|
+
if (fresh.length && win && !win.isDestroyed()) {
|
|
321
|
+
// newest arrival drives the peek. While dismissed, the same designed banner still
|
|
322
|
+
// shows — as a "ghost": the window appears for the notification only, and the
|
|
323
|
+
// renderer fades it out (never folding back into a pill) when it's done.
|
|
324
|
+
if (dismissed) {
|
|
325
|
+
if (hostRunning) {
|
|
326
|
+
ghostActive = true;
|
|
327
|
+
if (!win.isVisible()) win.showInactive();
|
|
328
|
+
win.webContents.send("newRelay", fresh[0], { ghost: true });
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
win.webContents.send("newRelay", fresh[0], { ghost: false });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
294
334
|
}
|
|
295
335
|
const sig = JSON.stringify({
|
|
296
336
|
relays: rows.map((r) => [r.id, r.relayNotificationKind, r.unread, r.title, r.senderName, r.urgency]),
|
|
@@ -648,7 +688,10 @@ function anchorTopRight() {
|
|
|
648
688
|
|
|
649
689
|
function maybeShow() {
|
|
650
690
|
if (!win || win.isDestroyed()) return;
|
|
651
|
-
|
|
691
|
+
// Dismissed keeps the window hidden except while a ghost notification is up.
|
|
692
|
+
// trayForcedVisible honors an explicit menu-bar click even when no host runs.
|
|
693
|
+
const wanted = (hostRunning || trayForcedVisible) && (!dismissed || ghostActive);
|
|
694
|
+
if (wanted) {
|
|
652
695
|
if (!win.isVisible()) win.showInactive();
|
|
653
696
|
} else if (win.isVisible()) {
|
|
654
697
|
win.hide();
|
|
@@ -662,6 +705,7 @@ function pollHosts() {
|
|
|
662
705
|
const claude = !err && /\/Claude\.app\/Contents\/MacOS\/Claude(\n|$)/.test(s);
|
|
663
706
|
const codex = !err && /\/Codex\.app\/Contents\/MacOS\/Codex(\n|$)/.test(s);
|
|
664
707
|
hostRunning = claude || codex;
|
|
708
|
+
if (hostRunning) trayForcedVisible = false; // normal host-based visibility takes back over
|
|
665
709
|
maybeShow();
|
|
666
710
|
});
|
|
667
711
|
frontmostBundleId((bundle) => {
|
|
@@ -730,6 +774,29 @@ function createWindow() {
|
|
|
730
774
|
setInterval(pollHosts, 1500);
|
|
731
775
|
}
|
|
732
776
|
|
|
777
|
+
// ---- menu bar (tray) -------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
// The Relay mark lives in the macOS menu bar. Clicking it brings the card back
|
|
780
|
+
// FULLY OPEN (never the folded pill) — the counterpart to the card's ✕, which
|
|
781
|
+
// hides the overlay entirely. The *Template naming makes macOS tint the mark
|
|
782
|
+
// for light/dark menu bars automatically.
|
|
783
|
+
function createTray() {
|
|
784
|
+
const icon = nativeImage.createFromPath(path.join(__dirname, "relayTrayTemplate.png"));
|
|
785
|
+
if (icon.isEmpty()) {
|
|
786
|
+
console.error("[overlay] tray icon missing; menu bar reopen unavailable");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
tray = new Tray(icon);
|
|
790
|
+
tray.setToolTip("Relay");
|
|
791
|
+
tray.on("click", () => {
|
|
792
|
+
setDismissed(false);
|
|
793
|
+
ghostActive = false;
|
|
794
|
+
if (!hostRunning) trayForcedVisible = true; // an explicit click always gets the card
|
|
795
|
+
maybeShow();
|
|
796
|
+
if (win && !win.isDestroyed()) win.webContents.send("openFull");
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
733
800
|
// ---- ipc -----------------------------------------------------------------
|
|
734
801
|
|
|
735
802
|
ipcMain.handle("relay:get", () => buildPayload());
|
|
@@ -772,6 +839,27 @@ ipcMain.on("relay:interactive", (_e, interactive) => {
|
|
|
772
839
|
ipcMain.on("relay:setPos", (_e, x, y) => {
|
|
773
840
|
if (win && !win.isDestroyed() && Number.isFinite(x) && Number.isFinite(y)) win.setPosition(Math.round(x), Math.round(y));
|
|
774
841
|
});
|
|
842
|
+
// ✕ on the card: hide the overlay entirely until the menu-bar Relay mark is clicked.
|
|
843
|
+
// The renderer plays its exit animation first, then sends this.
|
|
844
|
+
ipcMain.on("relay:dismiss", () => {
|
|
845
|
+
setDismissed(true);
|
|
846
|
+
ghostActive = false;
|
|
847
|
+
trayForcedVisible = false;
|
|
848
|
+
if (win && !win.isDestroyed() && win.isVisible()) win.hide();
|
|
849
|
+
});
|
|
850
|
+
// The user engaged with a ghost notification (tapped the lockup to expand it):
|
|
851
|
+
// the card is fully open again, so leave dismissed mode.
|
|
852
|
+
ipcMain.on("relay:undismiss", () => {
|
|
853
|
+
setDismissed(false);
|
|
854
|
+
ghostActive = false;
|
|
855
|
+
maybeShow();
|
|
856
|
+
});
|
|
857
|
+
// A ghost notification finished its exit animation; re-hide the window if the
|
|
858
|
+
// overlay is still dismissed.
|
|
859
|
+
ipcMain.on("relay:notifDone", () => {
|
|
860
|
+
ghostActive = false;
|
|
861
|
+
if (dismissed && win && !win.isDestroyed() && win.isVisible()) win.hide();
|
|
862
|
+
});
|
|
775
863
|
ipcMain.handle("relay:soundBytes", (_e, name) => {
|
|
776
864
|
const clean = String(name).replace(/[^A-Za-z]/g, "");
|
|
777
865
|
const candidates = [
|
|
@@ -799,6 +887,7 @@ if (!gotSingleInstanceLock) {
|
|
|
799
887
|
app.whenReady().then(() => {
|
|
800
888
|
if (process.platform === "darwin" && app.dock) app.dock.hide(); // accessory: no dock icon, no NC banners
|
|
801
889
|
createWindow();
|
|
890
|
+
createTray();
|
|
802
891
|
});
|
|
803
892
|
|
|
804
893
|
app.on("window-all-closed", () => {
|
package/overlay/preload.cjs
CHANGED
|
@@ -3,8 +3,9 @@ const { contextBridge, ipcRenderer } = require("electron");
|
|
|
3
3
|
contextBridge.exposeInMainWorld("relay", {
|
|
4
4
|
// streams from main -> renderer
|
|
5
5
|
onInbox: (cb) => ipcRenderer.on("inbox", (_e, payload) => cb(payload)),
|
|
6
|
-
onNewRelay: (cb) => ipcRenderer.on("newRelay", (_e, relay) => cb(relay)),
|
|
6
|
+
onNewRelay: (cb) => ipcRenderer.on("newRelay", (_e, relay, opts) => cb(relay, opts || {})),
|
|
7
7
|
onOpenDone: (cb) => ipcRenderer.on("openDone", (_e, id) => cb(id)),
|
|
8
|
+
onOpenFull: (cb) => ipcRenderer.on("openFull", () => cb()), // menu-bar icon clicked
|
|
8
9
|
|
|
9
10
|
// pull the full payload on demand
|
|
10
11
|
refresh: () => ipcRenderer.invoke("relay:get"),
|
|
@@ -34,4 +35,9 @@ contextBridge.exposeInMainWorld("relay", {
|
|
|
34
35
|
interactive: (v) => ipcRenderer.send("relay:interactive", v),
|
|
35
36
|
setPos: (x, y) => ipcRenderer.send("relay:setPos", x, y),
|
|
36
37
|
soundBytes: (name) => ipcRenderer.invoke("relay:soundBytes", name),
|
|
38
|
+
|
|
39
|
+
// dismissal (the card's ✕ / the menu-bar mark / ghost notifications)
|
|
40
|
+
dismiss: () => ipcRenderer.send("relay:dismiss"),
|
|
41
|
+
undismiss: () => ipcRenderer.send("relay:undismiss"),
|
|
42
|
+
notifDone: () => ipcRenderer.send("relay:notifDone"),
|
|
37
43
|
});
|
|
Binary file
|
|
Binary file
|