@parley-im/parley 0.2.1 → 0.2.3
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/package.json +1 -1
- package/web/app.css +33 -8
- package/web/chat.html +13 -6
- package/web/chat.js +13 -2
- package/web/dashboard.js +53 -16
- package/web/site.js +11 -0
package/package.json
CHANGED
package/web/app.css
CHANGED
|
@@ -74,6 +74,8 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|
|
74
74
|
.crumbs { display: flex; align-items: baseline; gap: 8px; min-width: 0; flex: 1; }
|
|
75
75
|
.crumbs .product { color: var(--muted); font-weight: 500; }
|
|
76
76
|
.crumbs .slash { color: var(--faint); }
|
|
77
|
+
.brand { display: inline-flex; align-items: center; gap: 10px; font-weight: 600; color: inherit; text-decoration: none !important; }
|
|
78
|
+
.brand:hover .mark { filter: brightness(1.1); }
|
|
77
79
|
.crumbs .here { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
78
80
|
.crumbs a { text-decoration: none; }
|
|
79
81
|
.crumbs a:hover { text-decoration: underline; }
|
|
@@ -109,13 +111,16 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|
|
109
111
|
.chip.low { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, var(--line)); }
|
|
110
112
|
.info-btn { width: 34px; height: 34px; margin-right: -6px; }
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
/* The menu: drops from the button at the top right, over the conversation. */
|
|
115
|
+
.sheet-back { position: absolute; inset: 0; z-index: 40; background: rgba(0, 0, 0, 0.12); }
|
|
113
116
|
.sheet {
|
|
114
|
-
position:
|
|
115
|
-
width: min(
|
|
117
|
+
position: absolute; z-index: 41; right: 8px; top: calc(54px + env(safe-area-inset-top));
|
|
118
|
+
width: min(380px, calc(100% - 16px)); max-height: calc(100% - 70px - env(safe-area-inset-top)); overflow-y: auto;
|
|
116
119
|
background: var(--panel); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow);
|
|
120
|
+
transform-origin: top right; animation: drop 0.14s ease-out;
|
|
117
121
|
}
|
|
118
|
-
.sheet header { display: flex; align-items: center; justify-content: space-between; padding:
|
|
122
|
+
.sheet header { display: flex; align-items: center; justify-content: space-between; padding: 8px 6px 2px 16px; }
|
|
123
|
+
.sheet header .icon-btn { width: 34px; height: 34px; }
|
|
119
124
|
.sheet h2 { font-size: 16px; margin: 0; }
|
|
120
125
|
.sheet-body { padding: 4px 16px 16px; font-size: 14px; }
|
|
121
126
|
.sheet-body p { margin: 0 0 10px; color: var(--muted); }
|
|
@@ -125,9 +130,6 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|
|
125
130
|
.sheet-body .fine { font-size: 12px; color: var(--faint); }
|
|
126
131
|
.sheet-body details.how { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
|
|
127
132
|
.sheet-body details.how summary { cursor: pointer; color: var(--text); font-weight: 500; margin-bottom: 8px; }
|
|
128
|
-
@media (max-width: 560px) {
|
|
129
|
-
.sheet { left: 0; right: 0; top: auto; bottom: 0; transform: none; width: auto; border-radius: 16px 16px 0 0; padding-bottom: env(safe-area-inset-bottom); }
|
|
130
|
-
}
|
|
131
133
|
|
|
132
134
|
.notice {
|
|
133
135
|
margin: 10px 16px 0; padding: 9px 12px; border-radius: 8px; font-size: 14px;
|
|
@@ -138,7 +140,7 @@ button, input, textarea, select { font: inherit; color: inherit; }
|
|
|
138
140
|
|
|
139
141
|
/* ---------- chat ---------- */
|
|
140
142
|
|
|
141
|
-
.app { display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }
|
|
143
|
+
.app { position: relative; display: flex; flex-direction: column; height: 100dvh; overflow: hidden; }
|
|
142
144
|
/* The chat never scrolls the page itself, only its conversation. */
|
|
143
145
|
body:has(> .app) { overflow: hidden; overscroll-behavior: none; }
|
|
144
146
|
.layout { flex: 1; min-height: 0; display: flex; }
|
|
@@ -393,6 +395,29 @@ body.panel { overflow: auto; }
|
|
|
393
395
|
.site code { font-family: var(--mono); font-size: 0.88em; background: var(--subtle); border: 1px solid var(--line); padding: 0.05em 0.35em; border-radius: 5px; }
|
|
394
396
|
.site h3 { margin: 32px 0 8px; }
|
|
395
397
|
.stack { display: grid; gap: 10px; max-width: 360px; margin-top: 16px; }
|
|
398
|
+
/* Your account, top right of parley.im's pages. */
|
|
399
|
+
.account-menu { position: relative; }
|
|
400
|
+
.account-menu summary { list-style: none; display: flex; align-items: center; gap: 4px; padding: 3px 6px 3px 3px; border-radius: 99px; cursor: pointer; }
|
|
401
|
+
.account-menu summary::-webkit-details-marker { display: none; }
|
|
402
|
+
.account-menu summary:hover, .account-menu[open] summary { background: var(--subtle); }
|
|
403
|
+
.account-menu summary svg { width: 14px; height: 14px; fill: none; stroke: var(--muted); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
404
|
+
.avatar { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); font-weight: 600; font-size: 13px; }
|
|
405
|
+
.menu-panel {
|
|
406
|
+
position: absolute; right: 0; top: calc(100% + 8px); z-index: 30; min-width: 220px; max-width: calc(100vw - 24px);
|
|
407
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); padding: 6px;
|
|
408
|
+
animation: drop 0.14s ease-out;
|
|
409
|
+
}
|
|
410
|
+
.menu-panel a, .menu-panel button {
|
|
411
|
+
display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 10px; border: 0; border-radius: 8px;
|
|
412
|
+
background: none; color: var(--text); text-decoration: none; font-size: 14px; text-align: left; cursor: pointer;
|
|
413
|
+
}
|
|
414
|
+
.menu-panel a:hover, .menu-panel button:hover { background: var(--subtle); }
|
|
415
|
+
.menu-panel form { margin: 0; border-top: 1px solid var(--line); margin-top: 4px; padding-top: 4px; }
|
|
416
|
+
.menu-who { display: grid; padding: 8px 10px 10px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
|
|
417
|
+
.menu-who strong { font-size: 14px; }
|
|
418
|
+
.menu-who span { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
419
|
+
@keyframes drop { from { opacity: 0; transform: translateY(-4px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
420
|
+
|
|
396
421
|
.live { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--muted); white-space: nowrap; }
|
|
397
422
|
.docs { padding-bottom: 40px; }
|
|
398
423
|
.docs h2 { font-size: 17px; letter-spacing: -0.01em; margin: 36px 0 10px; }
|
package/web/chat.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<span class="status" id="status" hidden><span class="dot" id="status-dot"></span><span id="status-text"></span></span>
|
|
25
25
|
<span class="chip" id="chip" hidden></span>
|
|
26
26
|
<button class="icon-btn info-btn" id="info-btn" type="button" title="This conversation" hidden>
|
|
27
|
-
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
27
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg>
|
|
28
28
|
<span class="sr">This conversation: details and actions</span>
|
|
29
29
|
</button>
|
|
30
30
|
</header>
|
|
@@ -66,12 +66,19 @@
|
|
|
66
66
|
</div>
|
|
67
67
|
<aside class="side" id="side" hidden></aside>
|
|
68
68
|
</div>
|
|
69
|
+
<!-- Inside the app, so it stays under the header when a phone's keyboard
|
|
70
|
+
moves things around. -->
|
|
71
|
+
<div class="sheet-back" id="sheet-back" hidden></div>
|
|
72
|
+
<section class="sheet" id="sheet" hidden aria-label="This conversation">
|
|
73
|
+
<header>
|
|
74
|
+
<h2>This conversation</h2>
|
|
75
|
+
<button class="icon-btn" id="sheet-close" type="button" title="Close">
|
|
76
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg><span class="sr">Close</span>
|
|
77
|
+
</button>
|
|
78
|
+
</header>
|
|
79
|
+
<div class="sheet-body" id="sheet-body"></div>
|
|
80
|
+
</section>
|
|
69
81
|
</div>
|
|
70
|
-
<div class="sheet-back" id="sheet-back" hidden></div>
|
|
71
|
-
<section class="sheet" id="sheet" hidden aria-label="This conversation">
|
|
72
|
-
<header><h2>This conversation</h2><button class="btn ghost" id="sheet-close" type="button">Done</button></header>
|
|
73
|
-
<div class="sheet-body" id="sheet-body"></div>
|
|
74
|
-
</section>
|
|
75
82
|
<div class="drop" id="drop" hidden><div>Drop screenshots to add them</div></div>
|
|
76
83
|
<script>window.PARLEY = {{CONFIG}};</script>
|
|
77
84
|
<script src="/static/markdown.js"></script>
|
package/web/chat.js
CHANGED
|
@@ -360,11 +360,18 @@
|
|
|
360
360
|
el("p", {}, `When the replies run out, the chat pauses until ${whom} adds more. Replies come from ${theirs} computer, so it needs to be on, and the link stops working for good once the chat ends.`),
|
|
361
361
|
),
|
|
362
362
|
);
|
|
363
|
+
// Just under the header, whatever its height on this screen.
|
|
364
|
+
const header = document.querySelector(".topbar").getBoundingClientRect();
|
|
365
|
+
els.sheet.style.top = `${header.bottom - els.app.getBoundingClientRect().top + 6}px`;
|
|
363
366
|
els.sheet.hidden = els.sheetBack.hidden = false;
|
|
367
|
+
els.infoBtn.setAttribute("aria-expanded", "true");
|
|
364
368
|
els.sheetClose.focus();
|
|
365
369
|
}
|
|
366
|
-
const closeSheet = () =>
|
|
367
|
-
|
|
370
|
+
const closeSheet = () => {
|
|
371
|
+
els.sheet.hidden = els.sheetBack.hidden = true;
|
|
372
|
+
els.infoBtn.setAttribute("aria-expanded", "false");
|
|
373
|
+
};
|
|
374
|
+
els.infoBtn.addEventListener("click", () => (els.sheet.hidden ? openSheet() : closeSheet()));
|
|
368
375
|
els.sheetClose.addEventListener("click", closeSheet);
|
|
369
376
|
els.sheetBack.addEventListener("click", closeSheet);
|
|
370
377
|
addEventListener("keydown", (e) => e.key === "Escape" && closeSheet());
|
|
@@ -736,6 +743,10 @@
|
|
|
736
743
|
clearTimeout(retry);
|
|
737
744
|
retry = null;
|
|
738
745
|
ws = new WebSocket(`${location.origin.replace(/^http/, "ws")}${base}/ws`);
|
|
746
|
+
// Keep-alives, answered by parley.im at the edge (no cost), keep phone
|
|
747
|
+
// networks and proxies from dropping a quiet connection.
|
|
748
|
+
const ping = setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('{"t":"ping"}'), 30_000);
|
|
749
|
+
ws.addEventListener("close", () => clearInterval(ping));
|
|
739
750
|
ws.onopen = () => {
|
|
740
751
|
failures = 0;
|
|
741
752
|
h.offline(false, "connection");
|
package/web/dashboard.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Keeps the parley.im dashboard current: who's on each session's page right
|
|
2
|
-
// now, when they were last active, and whether a reply is coming in.
|
|
3
|
-
//
|
|
2
|
+
// now, when they were last active, and whether a reply is coming in. Each
|
|
3
|
+
// linked computer pushes news over its own WebSocket, which costs nothing
|
|
4
|
+
// while it's quiet, so nothing here polls. It never shows what was said;
|
|
5
|
+
// parley.im doesn't have that.
|
|
4
6
|
(() => {
|
|
7
|
+
const REPLYING_MS = 6000;
|
|
8
|
+
const live = new Map(); // session id -> { here, active, replying, online }
|
|
9
|
+
|
|
5
10
|
const ago = (ms) => {
|
|
6
11
|
const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
|
|
7
12
|
if (s < 60) return "just now";
|
|
@@ -17,25 +22,57 @@
|
|
|
17
22
|
// Matches live() in relay/src/pages.ts.
|
|
18
23
|
const liveNodes = (s) => {
|
|
19
24
|
if (s.online === false) return dot("off", "computer offline");
|
|
20
|
-
if (s.replying) return dot("busy", "replying");
|
|
25
|
+
if (s.replying && Date.now() - s.replying < REPLYING_MS) return dot("busy", "replying");
|
|
21
26
|
if (s.here) return dot("on", s.here > 1 ? `${s.here} people here` : "here now");
|
|
22
27
|
if (s.active) return dot("", `active ${ago(s.active)}`);
|
|
23
28
|
return dot("", "not opened yet");
|
|
24
29
|
};
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
function paint() {
|
|
32
|
+
for (const row of document.querySelectorAll("[data-session]")) {
|
|
33
|
+
const s = live.get(row.dataset.session);
|
|
34
|
+
if (s) row.querySelector(".live")?.replaceChildren(...liveNodes(s));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function apply(computer, m) {
|
|
39
|
+
if (m.t === "changed") return location.reload();
|
|
40
|
+
if (m.t !== "live") return;
|
|
41
|
+
// Times come from parley.im's clock; shift them onto this one.
|
|
42
|
+
const shift = Date.now() - m.now;
|
|
43
|
+
for (const row of document.querySelectorAll(`[data-device="${computer}"]`)) {
|
|
44
|
+
const s = m.sessions[row.dataset.session] ?? {};
|
|
45
|
+
live.set(row.dataset.session, {
|
|
46
|
+
online: m.on,
|
|
47
|
+
here: s.here ?? 0,
|
|
48
|
+
active: s.active ? s.active + shift : null,
|
|
49
|
+
replying: s.replying ? s.replying + shift : null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const row = document.querySelector(`[data-computer="${computer}"]`);
|
|
53
|
+
if (row) {
|
|
54
|
+
row.querySelector(".dot").className = `dot${m.on ? " on" : ""}`;
|
|
55
|
+
row.querySelector(".computer-status").textContent = m.on ? `online now${m.version ? ` · parley ${m.version}` : ""}` : "offline";
|
|
56
|
+
}
|
|
57
|
+
paint();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function watch(computer, delay = 2000) {
|
|
61
|
+
const ws = new WebSocket(`${location.origin.replace(/^http/, "ws")}/dashboard/live?d=${encodeURIComponent(computer)}`);
|
|
62
|
+
const ping = setInterval(() => ws.readyState === WebSocket.OPEN && ws.send('{"t":"ping"}'), 30_000);
|
|
63
|
+
ws.onopen = () => (delay = 2000);
|
|
64
|
+
ws.onmessage = (event) => {
|
|
65
|
+
try {
|
|
66
|
+
apply(computer, JSON.parse(event.data));
|
|
67
|
+
} catch {}
|
|
68
|
+
};
|
|
69
|
+
ws.onclose = () => {
|
|
70
|
+
clearInterval(ping);
|
|
71
|
+
setTimeout(() => watch(computer, Math.min(delay * 2, 60_000)), delay);
|
|
72
|
+
};
|
|
37
73
|
}
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
for (const row of document.querySelectorAll("[data-computer]")) watch(row.dataset.computer);
|
|
76
|
+
// "active 3m ago" and the end of "replying" move on with the clock alone.
|
|
77
|
+
setInterval(paint, 5000);
|
|
41
78
|
})();
|
package/web/site.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// parley.im's account menu closes when you click outside it or press Escape.
|
|
2
|
+
(() => {
|
|
3
|
+
const menu = document.querySelector(".account-menu");
|
|
4
|
+
if (!menu) return;
|
|
5
|
+
document.addEventListener("click", (e) => {
|
|
6
|
+
if (menu.open && !menu.contains(e.target)) menu.open = false;
|
|
7
|
+
});
|
|
8
|
+
document.addEventListener("keydown", (e) => {
|
|
9
|
+
if (e.key === "Escape") menu.open = false;
|
|
10
|
+
});
|
|
11
|
+
})();
|