meta-sovereign 0.53.1 → 0.53.2

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/README.md CHANGED
@@ -61,7 +61,7 @@ The hosted app is the default UI: `serve` prints its address, <https://link-foun
61
61
 
62
62
  On macOS, interactive `serve` now handles the full setup: it protects this installation’s TLS key with Secure Enclave, detects existing certificate trust, requests trust only when needed, and opens the GitHub Pages app. For a headless launch use `node js/bin/meta-sovereign.js serve --trust=true --open=true`. `--trust=false` and `--open=false` opt out. The Swift helper needs the macOS Command Line Tools; `--hardware-key=false` explicitly opts into a file key on unsupported Macs. Hardware protection applies to the key on disk; Node uses it in memory. See [local server setup and security](docs/LOCAL-SERVER-SECURITY.md) for the exact access rules and limits.
63
63
 
64
- Open conversations load first and show loading or retry status until their history is ready. Earlier history loads automatically on scroll in Chat and Operator. Message selection and menus work across the full row; unread cards use refreshed Telegram counters and retain cached data during connection failures. All / Private / Groups / Channels filters remain available inside every Telegram folder. Drag the vertical dividers to resize columns; widths are remembered. Calls is in the main menu after Chat, and Telegram account editing is under Profile. The default dark controls use white line icons. The JS server keeps its store under your OS user: `~/Library/Application Support/Meta Sovereign/store` on macOS, `%APPDATA%/Meta Sovereign/store` on Windows, and `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store` on Linux. Starting `serve` from an older checkout migrates its `.meta-sovereign` store once, preserving messages, contacts, sessions, media and certificate; a compatibility link preserves old paths. Restarts and a different working directory reuse the same store. `--store` or `META_SOVEREIGN_STORE_DIR` override it. See [chat readiness and data storage](docs/CHAT-READINESS.md).
64
+ Open conversations load first. History loads automatically on scroll, with a compact indicator inside the message list while requesting or retrying; no history status panel or manual loading button is shown. Earlier history loads automatically on scroll in Chat and Operator. Message selection and menus work across the full row; unread cards use refreshed Telegram counters and retain cached data during connection failures. All / Private / Groups / Channels filters remain available inside every Telegram folder. Drag the vertical dividers to resize columns; widths are remembered. Calls is in the main menu after Chat, and Telegram account editing is under Profile. The default dark controls use white line icons. Buttons and action links have nonzero spacing and aligned control padding. Stories is off by default; Settings → Stories enables its display and requests. The JS server keeps its store under your OS user: `~/Library/Application Support/Meta Sovereign/store` on macOS, `%APPDATA%/Meta Sovereign/store` on Windows, and `${XDG_CONFIG_HOME:-~/.config}/Meta Sovereign/store` on Linux. Starting `serve` from an older checkout migrates its `.meta-sovereign` store once, preserving messages, contacts, sessions, media and certificate; a compatibility link preserves old paths. Restarts and a different working directory reuse the same store. `--store` or `META_SOVEREIGN_STORE_DIR` override it. See [chat readiness and data storage](docs/CHAT-READINESS.md).
65
65
 
66
66
  ## Connect the SPA to your server
67
67
 
@@ -22,6 +22,10 @@ import {
22
22
  definedOnly,
23
23
  defaultAddressRules,
24
24
  } from './address-form.js';
25
+ import {
26
+ telegramChatUrl,
27
+ isTelegramWebUrl,
28
+ } from '../sources/telegram-chat-url.js';
25
29
  import { addressFormOf } from './person-learn.js';
26
30
  import {
27
31
  batchStopReason,
@@ -300,12 +304,18 @@ export const templateFor = (templates, form) =>
300
304
  * where the platform documents one; `null` elsewhere (copy the text).
301
305
  *
302
306
  * - e-mail: RFC 6068 `mailto:<addr>?body=` (line breaks as %0D%0A).
303
- * - Telegram: `tg://user?id=<id>` opens the chat with that user (Bot
304
- * API docs); the text still has to be pasted, because Telegram has no
305
- * pre-fill parameter for a user chat.
307
+ * - Telegram: documented username/phone links can prefill text; numeric-only
308
+ * peers open the specific chat in Telegram Web (sign-in may be required).
306
309
  * - VK: `https://vk.com/im?sel=<peer>` opens the dialog with a user.
307
310
  */
308
- export const prefillLinkFor = ({ source, contact, chat, text }) => {
311
+ export const prefillLinkFor = ({
312
+ source,
313
+ contact,
314
+ chat,
315
+ text,
316
+ username,
317
+ phone,
318
+ }) => {
309
319
  const id = String(contact ?? '');
310
320
  const peer = String(chat ?? id);
311
321
  if (source === 'email' && /^[^\s@]+@[^\s@]+$/.test(id)) {
@@ -319,7 +329,10 @@ export const prefillLinkFor = ({ source, contact, chat, text }) => {
319
329
  }
320
330
  const tg = source === 'telegram' ? id.match(/^(?:user)?(\d+)$/) : null;
321
331
  if (tg) {
322
- return { url: `tg://user?id=${tg[1]}`, prefillsText: false };
332
+ const url = telegramChatUrl({ source, chat: tg[1], username, phone, text });
333
+ return url
334
+ ? { url, prefillsText: Boolean(text) && !isTelegramWebUrl(url) }
335
+ : null;
323
336
  }
324
337
  return null;
325
338
  };
@@ -20,7 +20,7 @@ import crypto from 'node:crypto';
20
20
  import { promises as fs } from 'node:fs';
21
21
  import net from 'node:net';
22
22
  import path from 'node:path';
23
- import tls from 'node:tls';
23
+ import http2 from 'node:http2';
24
24
  import { DIR_MODE, tighten, writeFileDurable } from '../storage/durable.js';
25
25
  import {
26
26
  certificateFingerprint,
@@ -265,8 +265,19 @@ export const listenHttpAndHttps = (
265
265
  host = '127.0.0.1'
266
266
  ) =>
267
267
  new Promise((resolve, reject) => {
268
- const secure = tls.createServer({ key, cert }, (socket) =>
269
- httpServer.emit('connection', socket)
268
+ // Multiplex API calls, media and event streams over HTTPS. HTTP/1's six
269
+ // browser connections can otherwise all wait behind long media downloads.
270
+ const secure = http2.createSecureServer(
271
+ { key, cert, allowHTTP1: true },
272
+ (req, res) => {
273
+ // Keep the existing strict Host and Origin checks for HTTP/2 too.
274
+ req.headers.host ??= req.headers[':authority'];
275
+ httpServer.emit('request', req, res);
276
+ }
277
+ );
278
+ secure.requestTimeout = httpServer.requestTimeout;
279
+ secure.on('upgrade', (req, socket, head) =>
280
+ httpServer.emit('upgrade', req, socket, head)
270
281
  );
271
282
  // A browser that does not trust the certificate yet hangs up.
272
283
  secure.on('tlsClientError', () => {});
@@ -69,7 +69,7 @@ export const handleEventRoutes = (store, req, res, p, ctx) => {
69
69
  res.writeHead(200, {
70
70
  'content-type': 'text/event-stream; charset=utf-8',
71
71
  'cache-control': 'no-cache, no-transform',
72
- connection: 'keep-alive',
72
+ ...(req.httpVersionMajor === 2 ? {} : { connection: 'keep-alive' }),
73
73
  'x-accel-buffering': 'no',
74
74
  });
75
75
  // Reconnect quickly after the server restarts.
@@ -377,6 +377,11 @@ const extrasOf = (user, full) => ({
377
377
  const botRevenueOf = (user, full) =>
378
378
  user.bot && full.canViewRevenue ? { canViewStarsRevenue: true } : {};
379
379
 
380
+ const contactReach = (user) => ({
381
+ username: user.username ?? null,
382
+ phone: user.phone ?? null,
383
+ });
384
+
380
385
  const fetchUser = async (client, Api, helpers, inputPeer, peers, chat) => {
381
386
  const storedPeer = inputPeer ?? (await peers.input(chat));
382
387
  const id = inputUserOf(Api, helpers, storedPeer, chat);
@@ -389,7 +394,7 @@ const fetchUser = async (client, Api, helpers, inputPeer, peers, chat) => {
389
394
  title: nameOf(user),
390
395
  members: 1,
391
396
  online,
392
- username: user.username ?? null,
397
+ ...contactReach(user),
393
398
  about: full.about ?? null,
394
399
  photo: hasPhoto(user.photo),
395
400
  pinned: full.pinnedMsgId ?? null,
@@ -0,0 +1,34 @@
1
+ // Normal Telegram clients ignore tg://user?id (a Bot API abstraction).
2
+ // Use documented username/phone/message links; numeric-only peers use Web A's
3
+ // signed-in chat route instead of launching a native app without a destination.
4
+ // References: core.telegram.org/api/links; Ajaxy/telegram-tt src/util/routing.ts.
5
+ export const telegramChatUrl = ({
6
+ source,
7
+ chat,
8
+ username,
9
+ phone,
10
+ lastId,
11
+ text,
12
+ } = {}) => {
13
+ if (source !== 'telegram' || !/^-?[1-9]\d*$/.test(String(chat))) {
14
+ return null;
15
+ }
16
+ const handle = String(username ?? '').replace(/^@/, '');
17
+ const number = String(phone ?? '').replace(/[\s()+-]/g, '');
18
+ const native = /^[a-zA-Z][a-zA-Z0-9_]{3,31}$/.test(handle)
19
+ ? `https://t.me/${handle}`
20
+ : /^[1-9]\d{6,14}$/.test(number)
21
+ ? `https://t.me/+${number}`
22
+ : null;
23
+ if (native) {
24
+ return text ? `${native}?text=${encodeURIComponent(text)}` : native;
25
+ }
26
+ const id = String(chat);
27
+ if (/^-100\d+$/.test(id) && /^[1-9]\d*$/.test(String(lastId))) {
28
+ return `https://t.me/c/${id.slice(4)}/${lastId}`;
29
+ }
30
+ return `https://web.telegram.org/a/#${id}`;
31
+ };
32
+
33
+ export const isTelegramWebUrl = (url) =>
34
+ String(url ?? '').startsWith('https://web.telegram.org/a/#');
@@ -15,6 +15,8 @@
15
15
  /* One height for every button, input and select; one header height
16
16
  the sticky drawer, rail and full-height chat measure against. */
17
17
  --control-h: 2.25rem;
18
+ --control-gap: 0.5rem;
19
+ --control-pad: 0.4rem 0.9rem;
18
20
  --radius: 8px;
19
21
  --topbar-h: 3.5rem;
20
22
  --bottom-nav-h: 4rem;
@@ -174,7 +176,7 @@ body {
174
176
  }
175
177
  .header-btn {
176
178
  width: var(--control-h);
177
- padding: 0;
179
+ padding: 0.25rem;
178
180
  }
179
181
  .btn-label {
180
182
  display: none;
@@ -193,7 +195,16 @@ body {
193
195
  flex-wrap: wrap;
194
196
  gap: 0.25rem;
195
197
  }
196
- .topbar nav button {
198
+ .topbar nav /* Action containers inherit a nonzero gap even when a new toolbar omits it.
199
+ Named layouts can choose a denser spacing; no margins fight their alignment. */
200
+ :where(*:has(> :is(button, a.button, [role='button']) ~ :is(button, a.button, [role='button']))) {
201
+ gap: var(--control-gap);
202
+ }
203
+ /* Inline actions in prose/table cells do not participate in flex/grid gaps. */
204
+ :where(p, td, dd, .meta) > :is(button, a.button) + :is(button, a.button) {
205
+ margin-inline-start: var(--control-gap);
206
+ }
207
+ button {
197
208
  background: transparent;
198
209
  color: var(--text-muted);
199
210
  border: 1px solid transparent;
@@ -426,6 +437,9 @@ main[data-view='chat'] > .web-mode-banner {
426
437
  min-height: 1px;
427
438
  }
428
439
 
440
+ .chat-list > .chat-item + .chat-item {
441
+ margin-block-start: 0.25rem;
442
+ }
429
443
  .chat-item {
430
444
  /* Rows out of sight are not laid out or painted (R-Y71). */
431
445
  content-visibility: auto;
@@ -1197,8 +1211,10 @@ main[data-view='chat'] > .web-mode-banner {
1197
1211
  a.button {
1198
1212
  display: inline-flex;
1199
1213
  align-items: center;
1214
+ justify-content: center;
1215
+ gap: var(--control-gap);
1200
1216
  min-height: var(--control-h);
1201
- padding: 0.4rem 0.9rem;
1217
+ padding: var(--control-pad);
1202
1218
  border: 1px solid var(--border);
1203
1219
  border-radius: var(--radius);
1204
1220
  background: var(--bg-active);
@@ -1409,7 +1425,7 @@ a.button {
1409
1425
  }
1410
1426
  .icon-btn {
1411
1427
  width: var(--control-h);
1412
- padding: 0;
1428
+ padding: 0.25rem;
1413
1429
  border-color: transparent;
1414
1430
  background: transparent;
1415
1431
  }
@@ -1522,7 +1538,7 @@ button {
1522
1538
  color: inherit;
1523
1539
  border: 1px solid var(--border);
1524
1540
  border-radius: var(--radius);
1525
- padding: 0.4rem 0.9rem;
1541
+ padding: var(--control-pad);
1526
1542
  font: inherit;
1527
1543
  font-size: 0.9rem;
1528
1544
  line-height: 1.2;
@@ -1551,7 +1567,7 @@ button.primary:hover:not(:disabled) {
1551
1567
  }
1552
1568
  .row {
1553
1569
  display: flex;
1554
- gap: 0.5rem;
1570
+ gap: var(--control-gap);
1555
1571
  align-items: center;
1556
1572
  flex-wrap: wrap;
1557
1573
  }
@@ -2833,6 +2849,10 @@ button.primary .recommended-tag {
2833
2849
  flex-direction: column;
2834
2850
  gap: 0.3rem;
2835
2851
  }
2852
+ .automation-list li {
2853
+ display: grid;
2854
+ gap: var(--control-gap);
2855
+ }
2836
2856
  .automation-list button {
2837
2857
  width: 100%;
2838
2858
  text-align: left;
@@ -3608,6 +3628,9 @@ button.primary .recommended-tag {
3608
3628
  rounded, frosted, an icon before each item; it grows from the corner
3609
3629
  it opened at. */
3610
3630
  .msg-context-menu {
3631
+ display: flex;
3632
+ flex-direction: column;
3633
+ gap: 0.25rem;
3611
3634
  position: fixed;
3612
3635
  /* Above a dialog: the files' Send button has a menu too (R-Y102). */
3613
3636
  z-index: 2100;
@@ -6791,7 +6814,7 @@ button[aria-busy='true']::before {
6791
6814
  scrollbar-width: thin;
6792
6815
  }
6793
6816
  .stories-strip.is-compact .stories-list {
6794
- gap: 0;
6817
+ gap: 0.25rem;
6795
6818
  }
6796
6819
  .story-peer {
6797
6820
  display: flex;
@@ -9184,7 +9207,7 @@ span.bubble-effect {
9184
9207
  left: 50%;
9185
9208
  top: 50%;
9186
9209
  transform: translate(-50%, -50%);
9187
- padding: 0.4rem 0.9rem;
9210
+ padding: var(--control-pad);
9188
9211
  border: 0;
9189
9212
  border-radius: 999px;
9190
9213
  background: rgba(0, 0, 0, 0.6);
@@ -12908,23 +12931,6 @@ audio.remote-player {
12908
12931
  flex-shrink: 0;
12909
12932
  scrollbar-width: thin;
12910
12933
  }
12911
- .chat-history-status {
12912
- display: flex;
12913
- flex: 0 0 auto;
12914
- align-items: center;
12915
- gap: 0.5rem;
12916
- padding: 0.5rem 0.75rem;
12917
- border-bottom: 1px solid var(--border);
12918
- color: var(--text);
12919
- background: var(--chat-sidebar);
12920
- font-size: 0.85rem;
12921
- }
12922
- .chat-history-status span {
12923
- flex: 1;
12924
- }
12925
- .chat-history-status button {
12926
- flex-shrink: 0;
12927
- }
12928
12934
  .calls-content {
12929
12935
  min-height: 12rem;
12930
12936
  }