domma-cms 0.112.0 → 0.112.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "0.112.0",
3
+ "version": "0.112.1",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -277,6 +277,9 @@ return {
277
277
  // State
278
278
  // ---------------------------------------------------------------
279
279
  let mailboxes = [];
280
+ let refreshTimer = null;
281
+ let refreshing = false;
282
+ let observedThreadPage = null;
280
283
  let currentAccountId = null;
281
284
  let folders = [];
282
285
  let currentFolder = 'INBOX';
@@ -554,6 +557,8 @@ return {
554
557
  openPanels.clear();
555
558
  window.removeEventListener('beforeunload', onBeforeUnload);
556
559
  cancelMarkRead();
560
+ clearInterval(refreshTimer);
561
+ refreshTimer = null;
557
562
  clearTimeout(undoTimer);
558
563
  if (keyboardHandler) document.removeEventListener('keydown', keyboardHandler);
559
564
  keyboardHandler = null;
@@ -1349,13 +1354,40 @@ return {
1349
1354
  // ---------------------------------------------------------------
1350
1355
 
1351
1356
  /**
1352
- * Fetch and draw the current page.
1357
+ * Check for arrivals before reloading, including in conversation mode.
1353
1358
  *
1354
1359
  * @returns {Promise<void>}
1355
1360
  */
1356
- async function loadMessages() {
1361
+ async function refreshMailbox() {
1362
+ if (refreshing || !currentAccountId) return;
1363
+ refreshing = true;
1364
+ const account = currentAccountId;
1365
+ try {
1366
+ if (features.search) {
1367
+ try {
1368
+ const result = await api(`${BASE}/sync/run?${new URLSearchParams({account})}`, 'POST');
1369
+ const failure = result.folders?.[currentFolder]?.mode === 'error'
1370
+ ? result.folders[currentFolder] : null;
1371
+ if (failure) throw new Error(failure.reason);
1372
+ } catch (err) {
1373
+ // Reduced mode still reads live from IMAP.
1374
+ if (err.status !== 503) throw err;
1375
+ }
1376
+ }
1377
+ if (!here() || currentAccountId !== account) return;
1378
+ page = 1;
1379
+ await loadMessages({fresh: true});
1380
+ } catch (err) {
1381
+ handleError(err);
1382
+ } finally {
1383
+ refreshing = false;
1384
+ }
1385
+ }
1386
+
1387
+ /** Fetch and draw the current page. */
1388
+ async function loadMessages({fresh = false} = {}) {
1357
1389
  if (searchQuery) return runSearch();
1358
- if (threadsOn) return loadThreads();
1390
+ if (threadsOn) return loadThreads({fresh});
1359
1391
 
1360
1392
  const loader = E.loader(listEl, {type: 'dots'});
1361
1393
  try {
@@ -1364,6 +1396,7 @@ return {
1364
1396
  folder: currentFolder,
1365
1397
  page: String(page)
1366
1398
  });
1399
+ if (fresh) params.set('fresh', '1');
1367
1400
  const result = await api(`${BASE}/messages?${params}`);
1368
1401
  lastListSource = result.source ?? 'imap';
1369
1402
  messages = result.messages;
@@ -1389,7 +1422,7 @@ return {
1389
1422
  *
1390
1423
  * @returns {Promise<void>}
1391
1424
  */
1392
- async function loadThreads() {
1425
+ async function loadThreads({fresh = false} = {}) {
1393
1426
  const loader = E.loader(listEl, {type: 'dots'});
1394
1427
  try {
1395
1428
  const params = new URLSearchParams({
@@ -1405,6 +1438,7 @@ return {
1405
1438
  // next folder may well be mirrored, and silently
1406
1439
  // forgetting the setting because one folder was not is
1407
1440
  // worse than one line of explanation.
1441
+ if (fresh) params.set('fresh', '1');
1408
1442
  const flat = await api(`${BASE}/messages?${params}`);
1409
1443
  lastListSource = flat.source ?? 'imap';
1410
1444
  messages = flat.messages;
@@ -4656,7 +4690,7 @@ return {
4656
4690
  id: `${pluginName}-refresh`,
4657
4691
  icon: 'refresh-cw',
4658
4692
  title: 'Refresh',
4659
- onClick: () => loadMessages()
4693
+ onClick: () => refreshMailbox()
4660
4694
  });
4661
4695
  if (refreshPlaced) $container.find('#mail-refresh-btn').get(0)?.remove();
4662
4696
  const placed = chrome.addBannerAction?.($container, {
@@ -4668,7 +4702,7 @@ return {
4668
4702
  if (placed) $container.find('#mail-settings-btn').get(0)?.remove();
4669
4703
  } catch { /* no banner: the pane buttons stay */ }
4670
4704
  if (!here()) return;
4671
- $container.find('#mail-refresh-btn').on('click', () => loadMessages());
4705
+ $container.find('#mail-refresh-btn').on('click', () => refreshMailbox());
4672
4706
 
4673
4707
  // Folder management was right-click only, which is the same as not
4674
4708
  // existing for anyone who does not already know it is there.
@@ -4760,6 +4794,31 @@ return {
4760
4794
 
4761
4795
  await start();
4762
4796
  if (!here()) return;
4797
+ // The sidebar updates independently; keep an open inbox in step with
4798
+ // the mirror without interrupting selection, search or older pages.
4799
+ refreshTimer = setInterval(async () => {
4800
+ if (!here() || document.hidden || refreshing || !currentAccountId ||
4801
+ page !== 1 || searchQuery || selected.size) return;
4802
+ const account = currentAccountId;
4803
+ const folder = currentFolder;
4804
+ refreshing = true;
4805
+ try {
4806
+ const params = new URLSearchParams({account, folder, page: '1'});
4807
+ const result = await api(`${BASE}/messages?${params}`);
4808
+ if (!here() || currentAccountId !== account || currentFolder !== folder ||
4809
+ page !== 1 || searchQuery || selected.size) return;
4810
+ const signature = rows => JSON.stringify(rows.map(m => [m.uid, m.flags]));
4811
+ const snapshot = JSON.stringify([account, folder, result.total, signature(result.messages)]);
4812
+ const changed = threadsOn
4813
+ ? snapshot !== observedThreadPage
4814
+ : result.total !== total || signature(result.messages) !== signature(messages);
4815
+ observedThreadPage = snapshot;
4816
+ if (changed) {
4817
+ await loadMessages();
4818
+ }
4819
+ } catch { /* A temporary failure must not replace the open reader. */ }
4820
+ finally { refreshing = false; }
4821
+ }, 30_000);
4763
4822
  Domma.icons.scan($container.get(0));
4764
4823
 
4765
4824
  // Domma's router does not call this; the lifetime does the work. Kept so a
@@ -385,7 +385,7 @@ export function registerReadRoutes(fastify, {auth, hooks, config, pool, pluginNa
385
385
  // answered from the store - instant, and no connection needed. It
386
386
  // returns null for anything not mirrored, which falls through to the
387
387
  // wire exactly as the free edition always does.
388
- if (cachedList) {
388
+ if (cachedList && request.query.fresh !== '1') {
389
389
  try {
390
390
  const cached = await cachedList({
391
391
  account: ctx.account, userId: ctx.uid, folder, page, limit