@profullstack/qryptchat 0.7.0 → 0.8.0
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/bin/qc.js +1 -1
- package/dist/{chunk-bdg6cmfy.js → chunk-nrv1k5q2.js} +105 -4
- package/dist/{chunk-g9xn5036.js → chunk-zc4n789c.js} +22 -1
- package/dist/commands.js +2 -2
- package/dist/mcp.js +2 -2
- package/dist/tui.js +123 -17
- package/package.json +1 -1
package/bin/qc.js
CHANGED
|
@@ -6,7 +6,7 @@ import { errorLine, exitWhenFlushed, main } from '../dist/commands.js';
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
7
|
|
|
8
8
|
main(process.argv.slice(2), { version: pkg.version }).then(
|
|
9
|
-
() => exitWhenFlushed(0),
|
|
9
|
+
() => exitWhenFlushed(process.exitCode ?? 0),
|
|
10
10
|
(err) => {
|
|
11
11
|
process.stderr.write(`${errorLine(err)}\n`);
|
|
12
12
|
exitWhenFlushed(1);
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
unlockSession,
|
|
7
7
|
storeSession,
|
|
8
8
|
clearSession,
|
|
9
|
-
notify
|
|
10
|
-
|
|
9
|
+
notify,
|
|
10
|
+
PAIRUX_URL,
|
|
11
|
+
newVoiceRoomLink,
|
|
12
|
+
voiceMessage
|
|
13
|
+
} from "./chunk-zc4n789c.js";
|
|
11
14
|
|
|
12
15
|
// src/cli/crypto.js
|
|
13
16
|
import { MlKem1024 as MlKem10242 } from "mlkem";
|
|
@@ -743,6 +746,9 @@ class QcClient {
|
|
|
743
746
|
throw new QcError(`HTTP ${res.status}`, res.status);
|
|
744
747
|
return res.text();
|
|
745
748
|
}
|
|
749
|
+
async archive(conversationId, on = true) {
|
|
750
|
+
return this.request(`/api/conversations/${on ? "archive" : "unarchive"}`, { method: "POST", body: { conversationId } });
|
|
751
|
+
}
|
|
746
752
|
async conversations() {
|
|
747
753
|
const { conversations = [] } = await this.request("/api/conversations/load", { method: "POST", body: {} });
|
|
748
754
|
return conversations.map((c) => ({ ...c, title: conversationTitle(c, this.me) }));
|
|
@@ -1181,6 +1187,55 @@ function who(user, { agent = false } = {}) {
|
|
|
1181
1187
|
return `${face}${name}${handle}${pronouns}`;
|
|
1182
1188
|
}
|
|
1183
1189
|
|
|
1190
|
+
// src/cli/voice.js
|
|
1191
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1192
|
+
var PAIRUX_INSTALL = "curl -fsSL https://installer.pairux.com/install.sh | bash";
|
|
1193
|
+
var VOICE_HELP = `qc voice: end-to-end encrypted voice rooms on PairUX (an alias for the pairux CLI)
|
|
1194
|
+
|
|
1195
|
+
Usage:
|
|
1196
|
+
qc voice [--send <chat>] [--json] print a link to a fresh anonymous room; --send posts it to a chat.
|
|
1197
|
+
Made on this machine: the key is in the #fragment, so PairUX
|
|
1198
|
+
never sees it, and nobody needs an account to join.
|
|
1199
|
+
qc voice login|logout|whoami sign the pairux CLI in or out
|
|
1200
|
+
qc voice join <CODE> | say | who | listen | status | leave | help
|
|
1201
|
+
any other subcommand runs \`pairux <subcommand>\`
|
|
1202
|
+
|
|
1203
|
+
The pairux CLI comes with the PairUX app: ${PAIRUX_INSTALL}
|
|
1204
|
+
PAIRUX_API_URL changes the server (default ${PAIRUX_URL}).`;
|
|
1205
|
+
function parseVoiceArgs(argv) {
|
|
1206
|
+
const [first] = argv;
|
|
1207
|
+
if (first === "help" || first === "--help" || first === "-h")
|
|
1208
|
+
return { action: "help" };
|
|
1209
|
+
if (first === undefined || first.startsWith("--") || first === "link" || first === "room" || first === "new") {
|
|
1210
|
+
const rest = first && !first.startsWith("--") ? argv.slice(1) : argv;
|
|
1211
|
+
const flags = { json: false, send: null };
|
|
1212
|
+
for (let i = 0;i < rest.length; i++) {
|
|
1213
|
+
if (rest[i] === "--json")
|
|
1214
|
+
flags.json = true;
|
|
1215
|
+
else if (rest[i] === "--send" && rest[i + 1])
|
|
1216
|
+
flags.send = rest[++i];
|
|
1217
|
+
else if (rest[i].startsWith("--send="))
|
|
1218
|
+
flags.send = rest[i].slice(7);
|
|
1219
|
+
else
|
|
1220
|
+
throw new Error(`Unknown option "${rest[i]}" for qc voice.
|
|
1221
|
+
|
|
1222
|
+
${VOICE_HELP}`);
|
|
1223
|
+
}
|
|
1224
|
+
return { action: "link", ...flags };
|
|
1225
|
+
}
|
|
1226
|
+
return { action: "pairux", args: argv };
|
|
1227
|
+
}
|
|
1228
|
+
var voiceLink = (env = process.env) => newVoiceRoomLink(env.PAIRUX_API_URL || PAIRUX_URL);
|
|
1229
|
+
function runPairux(args, { spawnImpl = spawn2, env = process.env } = {}) {
|
|
1230
|
+
return new Promise((resolve, reject) => {
|
|
1231
|
+
const child = spawnImpl(env.PAIRUX_BIN || "pairux", args, { stdio: "inherit", env });
|
|
1232
|
+
child.on("error", (err) => {
|
|
1233
|
+
reject(err.code === "ENOENT" ? new Error(`The pairux CLI is not installed. Install PairUX: ${PAIRUX_INSTALL}`) : err);
|
|
1234
|
+
});
|
|
1235
|
+
child.on("exit", (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1184
1239
|
// src/cli/exit.js
|
|
1185
1240
|
function exitWhenFlushed2(code, { timeoutMs = 30000 } = {}) {
|
|
1186
1241
|
setTimeout(() => process.exit(code), timeoutMs);
|
|
@@ -1202,7 +1257,10 @@ Usage:
|
|
|
1202
1257
|
qc profile [--emoji \uD83D\uDD2D] [--pronouns she/her] [--website URL] [--bio TEXT]
|
|
1203
1258
|
show or set your public profile (an empty value clears it);
|
|
1204
1259
|
--openprofile prints it as OpenProfile.md
|
|
1205
|
-
qc chats
|
|
1260
|
+
qc chats [--archived] list your chats (archived ones only with --archived)
|
|
1261
|
+
qc archive <chat> archive a chat for you (qc unarchive <chat> brings it back)
|
|
1262
|
+
qc voice [--send <chat>] a link to a fresh end-to-end encrypted PairUX voice room (no account
|
|
1263
|
+
needed to join); qc voice login|join|... runs the pairux CLI (qc voice help)
|
|
1206
1264
|
qc read <chat> [-n 20] print the last messages of a chat
|
|
1207
1265
|
qc send <chat> <text> send a message (text "-" reads stdin; --reply <message-id> to reply)
|
|
1208
1266
|
qc react <chat> <message-id> <emoji> [--remove] react to a message
|
|
@@ -1286,6 +1344,8 @@ async function main2(argv, { version = "0.0.0" } = {}) {
|
|
|
1286
1344
|
const out = (line) => process.stdout.write(`${line}
|
|
1287
1345
|
`);
|
|
1288
1346
|
const json = (value) => out(JSON.stringify(value, null, flags.json === "compact" ? 0 : 2));
|
|
1347
|
+
if (cmd === "voice")
|
|
1348
|
+
return voiceCommand(argv.slice(argv.indexOf("voice") + 1), { out, json });
|
|
1289
1349
|
if (flags.version)
|
|
1290
1350
|
return out(`qc ${version}`);
|
|
1291
1351
|
if (flags.help || cmd === "help")
|
|
@@ -1387,13 +1447,25 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
|
|
|
1387
1447
|
}
|
|
1388
1448
|
case "chats":
|
|
1389
1449
|
case "ls": {
|
|
1390
|
-
const
|
|
1450
|
+
const all = await (await client(flags)).conversations();
|
|
1451
|
+
const chats = all.filter((c) => !!c.is_archived === !!flags.archived);
|
|
1391
1452
|
if (flags.json)
|
|
1392
1453
|
return json(chats.map((c) => ({ id: c.id, title: c.title, type: c.type, updated_at: c.updated_at, participants: c.participants?.length ?? 0 })));
|
|
1393
1454
|
for (const c of chats)
|
|
1394
1455
|
out(`${c.id} ${mark(c.type === "group" ? "users" : "chat")}${c.title}`);
|
|
1395
1456
|
return;
|
|
1396
1457
|
}
|
|
1458
|
+
case "archive":
|
|
1459
|
+
case "unarchive": {
|
|
1460
|
+
if (!rest[0])
|
|
1461
|
+
throw new Error(`Usage: qc ${cmd} <chat>`);
|
|
1462
|
+
const c = await client(flags);
|
|
1463
|
+
const chat = findChat2(await c.conversations(), rest[0]);
|
|
1464
|
+
await c.archive(chat.id, cmd === "archive");
|
|
1465
|
+
if (flags.json)
|
|
1466
|
+
return json({ id: chat.id, archived: cmd === "archive" });
|
|
1467
|
+
return out(ok(cmd === "archive" ? `Archived ${chat.title}. qc chats --archived lists it; qc unarchive brings it back.` : `${chat.title} is back in your chats.`));
|
|
1468
|
+
}
|
|
1397
1469
|
case "read": {
|
|
1398
1470
|
if (!rest[0])
|
|
1399
1471
|
throw new Error("Usage: qc read <chat> [-n 20]");
|
|
@@ -1507,5 +1579,34 @@ ${info("Set: qc profile --emoji :telescope: --pronouns she/her --website https:/
|
|
|
1507
1579
|
${HELP2}`);
|
|
1508
1580
|
}
|
|
1509
1581
|
}
|
|
1582
|
+
async function voiceCommand(argv, { out, json }) {
|
|
1583
|
+
const parsed = parseVoiceArgs(argv);
|
|
1584
|
+
if (parsed.action === "help")
|
|
1585
|
+
return out(VOICE_HELP);
|
|
1586
|
+
if (parsed.action === "pairux") {
|
|
1587
|
+
const code = await runPairux(parsed.args);
|
|
1588
|
+
if (code)
|
|
1589
|
+
process.exitCode = code;
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const url = voiceLink();
|
|
1593
|
+
let sent = null;
|
|
1594
|
+
if (parsed.send) {
|
|
1595
|
+
const c = await client({});
|
|
1596
|
+
const chat = findChat2(await c.conversations(), parsed.send);
|
|
1597
|
+
const { skipped } = await c.send(chat.id, voiceMessage(url));
|
|
1598
|
+
sent = { id: chat.id, title: chat.title, skipped };
|
|
1599
|
+
}
|
|
1600
|
+
if (parsed.json)
|
|
1601
|
+
return json({ url, sent });
|
|
1602
|
+
out(url);
|
|
1603
|
+
if (sent) {
|
|
1604
|
+
if (sent.skipped)
|
|
1605
|
+
process.stderr.write(`qc: ${warn(`${sent.skipped} participant(s) have no key yet and will not see it.`)}
|
|
1606
|
+
`);
|
|
1607
|
+
process.stderr.write(`qc: ${mark("send")}Sent to ${sent.title}.
|
|
1608
|
+
`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1510
1611
|
|
|
1511
1612
|
export { exitWhenFlushed2, errorLine2, HELP2, parseArgs2, findChat2, main2 };
|
|
@@ -214,4 +214,25 @@ function notify(title, body, { env = process.env, streams = [process.stdout, pro
|
|
|
214
214
|
return Boolean(seq && tty);
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
|
|
217
|
+
// src/lib/chat/voice.js
|
|
218
|
+
var PAIRUX_URL = "https://pairux.com";
|
|
219
|
+
var toBase64Url = (bytes) => {
|
|
220
|
+
let s = "";
|
|
221
|
+
for (const b of bytes)
|
|
222
|
+
s += String.fromCharCode(b);
|
|
223
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
224
|
+
};
|
|
225
|
+
function newVoiceRoomLink(base = PAIRUX_URL) {
|
|
226
|
+
const room = toBase64Url(crypto.getRandomValues(new Uint8Array(16)));
|
|
227
|
+
const key = toBase64Url(crypto.getRandomValues(new Uint8Array(32)));
|
|
228
|
+
return `${String(base).replace(/\/$/, "")}/r/${room}#k=${key}`;
|
|
229
|
+
}
|
|
230
|
+
var voiceMessage = (url) => `\uD83C\uDF99️ Join my end-to-end encrypted voice room (no account needed): ${url}`;
|
|
231
|
+
function parseSlashCommand(text, names) {
|
|
232
|
+
const m = /^\/([a-z]+)(?:\s+(.*))?$/s.exec(String(text).trim());
|
|
233
|
+
if (!m || !names.includes(m[1]))
|
|
234
|
+
return null;
|
|
235
|
+
return { name: m[1], args: (m[2] ?? "").trim() };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { DEFAULT_URL, configDir, sessionPath, baseUrl, loadSession, unlockSession, storeSession, clearSession, notify, PAIRUX_URL, newVoiceRoomLink, voiceMessage, parseSlashCommand };
|
package/dist/commands.js
CHANGED
package/dist/mcp.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configDir,
|
|
3
|
-
notify
|
|
4
|
-
|
|
3
|
+
notify,
|
|
4
|
+
PAIRUX_URL,
|
|
5
|
+
newVoiceRoomLink,
|
|
6
|
+
voiceMessage,
|
|
7
|
+
parseSlashCommand
|
|
8
|
+
} from "./chunk-zc4n789c.js";
|
|
5
9
|
|
|
6
10
|
// src/cli/tui.js
|
|
7
11
|
import { createApp, createImageStore, drawIcon, drawRichText, editText, emojify, insertText, stringWidth, truncate, widgets, wrap } from "@profullstack/hqtui";
|
|
8
12
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
9
13
|
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
// src/lib/chat/membership-events.js
|
|
16
|
+
var MEMBERSHIP_EVENTS = {
|
|
17
|
+
JOINED: "PARTICIPANTS_JOINED",
|
|
18
|
+
LEFT: "PARTICIPANT_LEFT",
|
|
19
|
+
ARCHIVED: "CONVERSATION_ARCHIVED"
|
|
20
|
+
};
|
|
21
|
+
var CHAT_LIST_EVENTS = new Set(["CONVERSATION_CREATED", ...Object.values(MEMBERSHIP_EVENTS)]);
|
|
22
|
+
|
|
23
|
+
// src/cli/tui.js
|
|
10
24
|
var SIDEBAR = 30;
|
|
11
25
|
function initialState(me) {
|
|
12
26
|
return {
|
|
@@ -24,9 +38,17 @@ function initialState(me) {
|
|
|
24
38
|
scrollBack: 0,
|
|
25
39
|
status: "connecting",
|
|
26
40
|
notice: "",
|
|
27
|
-
loading: true
|
|
41
|
+
loading: true,
|
|
42
|
+
showArchived: false
|
|
28
43
|
};
|
|
29
44
|
}
|
|
45
|
+
var listed = (state) => state.conversations.filter((c) => !!c.is_archived === !!state.showArchived);
|
|
46
|
+
var CHAT_LIST_REFRESH_MS = 15000;
|
|
47
|
+
function chatsWithNews(prev, next) {
|
|
48
|
+
const seen = new Map(prev.map((c) => [c.id, c.updated_at ?? ""]));
|
|
49
|
+
return next.filter((c) => !seen.has(c.id) || (c.updated_at ?? "") > seen.get(c.id)).map((c) => c.id);
|
|
50
|
+
}
|
|
51
|
+
var SLASH_COMMANDS = ["voice", "archive", "unarchive", "archived"];
|
|
30
52
|
var pad2 = (n) => String(n).padStart(2, "0");
|
|
31
53
|
var timeOf = (iso) => {
|
|
32
54
|
const d = new Date(iso);
|
|
@@ -132,10 +154,13 @@ function render({ ui, width, height, theme }, state, on = {}) {
|
|
|
132
154
|
});
|
|
133
155
|
ui.row({ size: "fill" }, (row) => {
|
|
134
156
|
const listWidth = Math.min(SIDEBAR, Math.max(18, Math.floor(width * 0.3)));
|
|
135
|
-
|
|
157
|
+
const chats = listed(state);
|
|
158
|
+
const archivedCount = state.conversations.filter((c) => c.is_archived).length;
|
|
159
|
+
const listTitle = state.showArchived ? `Archived (${archivedCount})` : archivedCount ? `Chats · ${archivedCount} archived` : "Chats";
|
|
160
|
+
row.panel({ title: truncate(listTitle, Math.max(4, listWidth - 4)), size: listWidth, borderColor: state.focus === "list" ? theme?.borderFocused : undefined }, (p) => {
|
|
136
161
|
const inner = Math.max(4, listWidth - 5);
|
|
137
162
|
p.list({
|
|
138
|
-
items:
|
|
163
|
+
items: chats.map((c) => {
|
|
139
164
|
const unread = state.unread[c.id] || 0;
|
|
140
165
|
const tag = unread ? ` ●${unread > 99 ? "99+" : unread}` : "";
|
|
141
166
|
const name = truncate(c.title, inner - stringWidth(tag));
|
|
@@ -177,6 +202,8 @@ function render({ ui, width, height, theme }, state, on = {}) {
|
|
|
177
202
|
{ key: "Ctrl+E", label: "emoji" },
|
|
178
203
|
{ key: "Tab", label: state.focus === "list" ? "compose" : "chats" },
|
|
179
204
|
{ key: "PgUp", label: "scroll" },
|
|
205
|
+
...state.focus === "list" ? [{ key: "a", label: state.showArchived ? "unarchive" : "archive" }] : [],
|
|
206
|
+
{ key: "Ctrl+O", label: state.showArchived ? "chats" : "archived" },
|
|
180
207
|
{ key: "Ctrl+C", label: "quit" }
|
|
181
208
|
],
|
|
182
209
|
size: 1
|
|
@@ -241,16 +268,33 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
241
268
|
}
|
|
242
269
|
}
|
|
243
270
|
let keysChecked = false;
|
|
244
|
-
|
|
271
|
+
let listLoading = false;
|
|
272
|
+
async function loadConversations({ periodic = false } = {}) {
|
|
273
|
+
if (listLoading)
|
|
274
|
+
return;
|
|
275
|
+
listLoading = true;
|
|
245
276
|
try {
|
|
246
277
|
const before = state.activeId;
|
|
278
|
+
const prev = state.conversations;
|
|
247
279
|
state.conversations = await client.conversations();
|
|
248
280
|
state.loading = false;
|
|
249
|
-
if (
|
|
250
|
-
const
|
|
281
|
+
if (periodic) {
|
|
282
|
+
for (const id of chatsWithNews(prev, state.conversations)) {
|
|
283
|
+
if (id === state.activeId)
|
|
284
|
+
loadMessages(id);
|
|
285
|
+
else {
|
|
286
|
+
delete state.messages[id];
|
|
287
|
+
if (state.status !== "live" && prev.some((c) => c.id === id))
|
|
288
|
+
state.unread[id] = (state.unread[id] || 0) + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const chats = listed(state);
|
|
293
|
+
if (!state.activeId && chats.length) {
|
|
294
|
+
const want = initialChat ? chats.findIndex((c) => c.id === initialChat || c.title.toLowerCase().includes(String(initialChat).toLowerCase())) : 0;
|
|
251
295
|
openAt(Math.max(0, want));
|
|
252
296
|
} else {
|
|
253
|
-
state.selected = Math.max(0,
|
|
297
|
+
state.selected = Math.max(0, chats.findIndex((c) => c.id === before));
|
|
254
298
|
}
|
|
255
299
|
redraw();
|
|
256
300
|
if (!keysChecked) {
|
|
@@ -260,11 +304,14 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
260
304
|
}
|
|
261
305
|
} catch (err) {
|
|
262
306
|
state.loading = false;
|
|
263
|
-
|
|
307
|
+
if (!periodic || err.status === 401)
|
|
308
|
+
note(err.status === 401 ? "Session ended: run qc login" : `Could not load chats: ${err.message}`);
|
|
309
|
+
} finally {
|
|
310
|
+
listLoading = false;
|
|
264
311
|
}
|
|
265
312
|
}
|
|
266
313
|
function openAt(i) {
|
|
267
|
-
const c = state
|
|
314
|
+
const c = listed(state)[i];
|
|
268
315
|
if (!c)
|
|
269
316
|
return;
|
|
270
317
|
state.selected = i;
|
|
@@ -285,8 +332,50 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
285
332
|
client.typing(state.activeId, true);
|
|
286
333
|
}
|
|
287
334
|
}
|
|
288
|
-
|
|
289
|
-
|
|
335
|
+
function toggleArchivedView() {
|
|
336
|
+
state.showArchived = !state.showArchived;
|
|
337
|
+
const chats = listed(state);
|
|
338
|
+
state.selected = Math.max(0, chats.findIndex((c) => c.id === state.activeId));
|
|
339
|
+
if (!state.showArchived && !chats.length)
|
|
340
|
+
note("No chats here yet.");
|
|
341
|
+
if (state.showArchived && !chats.length)
|
|
342
|
+
note("Nothing archived. Press a on a chat (or type /archive) to archive it.");
|
|
343
|
+
redraw();
|
|
344
|
+
}
|
|
345
|
+
async function setArchived(chat, on) {
|
|
346
|
+
try {
|
|
347
|
+
await client.archive(chat.id, on);
|
|
348
|
+
chat.is_archived = on;
|
|
349
|
+
const chats = listed(state);
|
|
350
|
+
state.selected = Math.max(0, Math.min(state.selected, chats.length - 1));
|
|
351
|
+
note(on ? `Archived ${chat.title} (Ctrl+O shows archived chats)` : `${chat.title} is back in your chats`);
|
|
352
|
+
} catch (err) {
|
|
353
|
+
note(`Could not ${on ? "archive" : "unarchive"}: ${err.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function runSlash(raw) {
|
|
357
|
+
const cmd = parseSlashCommand(raw, SLASH_COMMANDS);
|
|
358
|
+
if (!cmd)
|
|
359
|
+
return false;
|
|
360
|
+
const active = state.conversations.find((c) => c.id === state.activeId);
|
|
361
|
+
state.field = { value: "", cursor: 0 };
|
|
362
|
+
if (cmd.name === "archived")
|
|
363
|
+
toggleArchivedView();
|
|
364
|
+
else if (!active)
|
|
365
|
+
note("Pick a chat first");
|
|
366
|
+
else if (cmd.name === "voice") {
|
|
367
|
+
const url = newVoiceRoomLink(process.env.PAIRUX_API_URL || PAIRUX_URL);
|
|
368
|
+
state.field = { value: voiceMessage(url), cursor: 0 };
|
|
369
|
+
send({ raw: true }).then(() => note("Voice room posted; open the link in a browser to join", { sticky: false }));
|
|
370
|
+
} else
|
|
371
|
+
setArchived(active, cmd.name === "archive");
|
|
372
|
+
redraw();
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
async function send({ raw = false } = {}) {
|
|
376
|
+
if (!raw && runSlash(state.field.value))
|
|
377
|
+
return;
|
|
378
|
+
const text = raw ? state.field.value.trim() : emojify(state.field.value.trim(), { mode: "emoji" });
|
|
290
379
|
if (!text || !state.activeId)
|
|
291
380
|
return;
|
|
292
381
|
const id = state.activeId;
|
|
@@ -350,9 +439,11 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
350
439
|
} else if (event.key === "pagedown") {
|
|
351
440
|
handlers.scroll(-4);
|
|
352
441
|
} else if (event.key === "ctrl+n" || event.key === "alt+down") {
|
|
353
|
-
openAt(Math.min(state.
|
|
442
|
+
openAt(Math.min(listed(state).length - 1, state.selected + 1));
|
|
354
443
|
} else if (event.key === "ctrl+p" || event.key === "alt+up") {
|
|
355
444
|
openAt(Math.max(0, state.selected - 1));
|
|
445
|
+
} else if (event.key === "ctrl+o") {
|
|
446
|
+
toggleArchivedView();
|
|
356
447
|
} else if (event.key === "ctrl+r") {
|
|
357
448
|
loadConversations();
|
|
358
449
|
if (state.activeId)
|
|
@@ -361,9 +452,14 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
361
452
|
if (event.key === "up" || event.key === "k")
|
|
362
453
|
state.selected = Math.max(0, state.selected - 1);
|
|
363
454
|
else if (event.key === "down" || event.key === "j")
|
|
364
|
-
state.selected = Math.min(state.
|
|
455
|
+
state.selected = Math.min(listed(state).length - 1, state.selected + 1);
|
|
365
456
|
else if (event.key === "enter" || event.key === "right" || event.key === "l")
|
|
366
457
|
handlers.open(state.selected);
|
|
458
|
+
else if (event.key === "a") {
|
|
459
|
+
const c = listed(state)[state.selected];
|
|
460
|
+
if (c)
|
|
461
|
+
setArchived(c, !c.is_archived);
|
|
462
|
+
}
|
|
367
463
|
} else if (event.key === "enter") {
|
|
368
464
|
send();
|
|
369
465
|
} else if (event.key === "up" && !state.field.value) {
|
|
@@ -386,7 +482,10 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
386
482
|
state.field = insertText(state.field, event.text);
|
|
387
483
|
redraw();
|
|
388
484
|
});
|
|
485
|
+
const listTimer = setInterval(() => loadConversations({ periodic: true }), CHAT_LIST_REFRESH_MS);
|
|
486
|
+
listTimer.unref?.();
|
|
389
487
|
app.on("exit", () => {
|
|
488
|
+
clearInterval(listTimer);
|
|
390
489
|
state.images?.clear();
|
|
391
490
|
abort.abort();
|
|
392
491
|
});
|
|
@@ -412,7 +511,7 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
412
511
|
if (i > 0) {
|
|
413
512
|
const [c] = state.conversations.splice(i, 1);
|
|
414
513
|
state.conversations.unshift(c);
|
|
415
|
-
state.selected = state.
|
|
514
|
+
state.selected = Math.max(0, listed(state).findIndex((x) => x.id === state.activeId));
|
|
416
515
|
}
|
|
417
516
|
if (i === -1)
|
|
418
517
|
loadConversations();
|
|
@@ -430,7 +529,9 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
430
529
|
}, 6100);
|
|
431
530
|
} else
|
|
432
531
|
delete room[data.userId];
|
|
433
|
-
} else if (type
|
|
532
|
+
} else if (CHAT_LIST_EVENTS.has(type)) {
|
|
533
|
+
if (type === MEMBERSHIP_EVENTS.LEFT && data?.userId === client.me?.id && data?.conversationId === state.activeId)
|
|
534
|
+
state.activeId = null;
|
|
434
535
|
loadConversations();
|
|
435
536
|
}
|
|
436
537
|
redraw();
|
|
@@ -442,10 +543,15 @@ async function runTui(client, { initialChat } = {}) {
|
|
|
442
543
|
}
|
|
443
544
|
}).catch((err) => note(err.message));
|
|
444
545
|
await app.start();
|
|
546
|
+
clearInterval(listTimer);
|
|
445
547
|
abort.abort();
|
|
446
548
|
}
|
|
447
549
|
export {
|
|
550
|
+
CHAT_LIST_REFRESH_MS,
|
|
551
|
+
SLASH_COMMANDS,
|
|
552
|
+
chatsWithNews,
|
|
448
553
|
initialState,
|
|
554
|
+
listed,
|
|
449
555
|
render,
|
|
450
556
|
runTui,
|
|
451
557
|
transcriptLines
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/qryptchat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "qc: qrypt.chat in your terminal. A full-screen end-to-end encrypted chat client (ML-KEM-1024), plus a scriptable CLI and an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|