@bobfrankston/rmfmail 1.2.222 → 1.2.224

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.
Files changed (65) hide show
  1. package/bin/mailx.js +6 -9
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +6 -9
  4. package/client/android-bootstrap.bundle.js +38 -4
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/client/app.bundle.js +149 -32
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/components/address-book.js +93 -29
  9. package/client/components/address-book.js.map +1 -1
  10. package/client/components/address-book.ts +103 -24
  11. package/client/components/context-menu.js +76 -0
  12. package/client/components/context-menu.js.map +1 -1
  13. package/client/components/context-menu.ts +77 -0
  14. package/client/compose/compose.bundle.js +276 -116
  15. package/client/compose/compose.bundle.js.map +4 -4
  16. package/client/compose/compose.js +4 -149
  17. package/client/compose/compose.js.map +1 -1
  18. package/client/compose/compose.ts +4 -104
  19. package/client/compose/edit-commands.js +207 -0
  20. package/client/compose/edit-commands.js.map +1 -0
  21. package/client/compose/edit-commands.ts +181 -0
  22. package/client/compose/editor.js +6 -0
  23. package/client/compose/editor.js.map +1 -1
  24. package/client/compose/editor.ts +6 -0
  25. package/client/compose/spellcheck.js +19 -0
  26. package/client/compose/spellcheck.js.map +1 -1
  27. package/client/compose/spellcheck.ts +11 -0
  28. package/client/lib/api-client.js +2 -2
  29. package/client/lib/api-client.js.map +1 -1
  30. package/client/lib/api-client.ts +2 -2
  31. package/client/lib/mailxapi.js +2 -2
  32. package/client/styles/components.css +40 -0
  33. package/package.json +7 -7
  34. package/packages/mailx-imap/index.d.ts.map +1 -1
  35. package/packages/mailx-imap/index.js +32 -4
  36. package/packages/mailx-imap/index.js.map +1 -1
  37. package/packages/mailx-imap/index.ts +34 -5
  38. package/packages/mailx-imap/package-lock.json +2 -2
  39. package/packages/mailx-imap/package.json +1 -1
  40. package/packages/mailx-service/index.d.ts +1 -1
  41. package/packages/mailx-service/index.d.ts.map +1 -1
  42. package/packages/mailx-service/index.js +33 -4
  43. package/packages/mailx-service/index.js.map +1 -1
  44. package/packages/mailx-service/index.ts +28 -4
  45. package/packages/mailx-service/jsonrpc.js +1 -1
  46. package/packages/mailx-service/jsonrpc.js.map +1 -1
  47. package/packages/mailx-service/jsonrpc.ts +1 -1
  48. package/packages/mailx-service/package.json +1 -1
  49. package/packages/mailx-store/db.d.ts +30 -10
  50. package/packages/mailx-store/db.d.ts.map +1 -1
  51. package/packages/mailx-store/db.js +108 -10
  52. package/packages/mailx-store/db.js.map +1 -1
  53. package/packages/mailx-store/db.ts +126 -13
  54. package/packages/mailx-store/package.json +1 -1
  55. package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
  56. package/packages/mailx-store-web/android-bootstrap.js +17 -2
  57. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  58. package/packages/mailx-store-web/android-bootstrap.ts +17 -2
  59. package/packages/mailx-store-web/db.d.ts +1 -1
  60. package/packages/mailx-store-web/db.d.ts.map +1 -1
  61. package/packages/mailx-store-web/db.js +17 -2
  62. package/packages/mailx-store-web/db.js.map +1 -1
  63. package/packages/mailx-store-web/db.ts +18 -3
  64. package/packages/mailx-store-web/package.json +1 -1
  65. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-104584 → node_modules.npmglobalize-stash-142920}/.package-lock.json +0 -0
@@ -306,8 +306,8 @@ function hasBccHistoryTo(email) {
306
306
  function listContacts(query, page = 1, pageSize = 100) {
307
307
  return ipc().listContacts(query, page, pageSize);
308
308
  }
309
- function upsertContact(name, email) {
310
- return ipc().upsertContact(name, email);
309
+ function upsertContact(name, email, fields) {
310
+ return ipc().upsertContact(name, email, fields);
311
311
  }
312
312
  function deleteContact(email) {
313
313
  return ipc().deleteContact(email);
@@ -723,7 +723,14 @@ __export(context_menu_exports, {
723
723
  closeContextMenu: () => closeContextMenu,
724
724
  showContextMenu: () => showContextMenu
725
725
  });
726
+ function closeSubmenu() {
727
+ if (activeSubmenu) {
728
+ activeSubmenu.remove();
729
+ activeSubmenu = null;
730
+ }
731
+ }
726
732
  function closeContextMenu() {
733
+ closeSubmenu();
727
734
  if (activeMenu) {
728
735
  activeMenu.remove();
729
736
  activeMenu = null;
@@ -737,6 +744,45 @@ function closeContextMenu() {
737
744
  escapeListener = null;
738
745
  }
739
746
  }
747
+ function openSubmenu(parentRow, items) {
748
+ closeSubmenu();
749
+ const sub = document.createElement("div");
750
+ sub.className = "ctx-menu";
751
+ for (const item of items) {
752
+ if (item.separator) {
753
+ const sep = document.createElement("div");
754
+ sep.className = "ctx-sep";
755
+ sub.appendChild(sep);
756
+ continue;
757
+ }
758
+ const el = document.createElement("div");
759
+ el.className = "ctx-item" + (item.disabled ? " ctx-disabled" : "");
760
+ el.textContent = item.label;
761
+ if (item.tooltip)
762
+ el.title = item.tooltip;
763
+ if (!item.disabled) {
764
+ el.addEventListener("click", () => {
765
+ closeContextMenu();
766
+ item.action();
767
+ });
768
+ }
769
+ sub.appendChild(el);
770
+ }
771
+ document.body.appendChild(sub);
772
+ const anchor = parentRow.getBoundingClientRect();
773
+ const vw = window.visualViewport?.width ?? window.innerWidth;
774
+ const vh = window.visualViewport?.height ?? window.innerHeight;
775
+ const rect = sub.getBoundingClientRect();
776
+ let left = anchor.right - 2;
777
+ if (left + rect.width > vw)
778
+ left = anchor.left - rect.width + 2;
779
+ let top = anchor.top;
780
+ if (top + rect.height > vh)
781
+ top = vh - rect.height - 4;
782
+ sub.style.left = `${Math.max(4, left)}px`;
783
+ sub.style.top = `${Math.max(4, top)}px`;
784
+ activeSubmenu = sub;
785
+ }
740
786
  function showContextMenu(x, y, items) {
741
787
  closeContextMenu();
742
788
  const menu = document.createElement("div");
@@ -755,6 +801,24 @@ function showContextMenu(x, y, items) {
755
801
  el.textContent = item.label;
756
802
  if (item.tooltip)
757
803
  el.title = item.tooltip;
804
+ if (item.submenu && item.submenu.length > 0 && !item.disabled) {
805
+ el.style.display = "flex";
806
+ el.style.justifyContent = "space-between";
807
+ el.style.gap = "12px";
808
+ const caret = document.createElement("span");
809
+ caret.textContent = "\u25B8";
810
+ caret.style.opacity = "0.6";
811
+ el.appendChild(caret);
812
+ const open = () => openSubmenu(el, item.submenu);
813
+ el.addEventListener("mouseenter", open);
814
+ el.addEventListener("click", (e) => {
815
+ e.stopPropagation();
816
+ open();
817
+ });
818
+ menu.appendChild(el);
819
+ continue;
820
+ }
821
+ el.addEventListener("mouseenter", closeSubmenu);
758
822
  if (!item.disabled) {
759
823
  el.addEventListener("click", () => {
760
824
  closeContextMenu();
@@ -786,6 +850,8 @@ function showContextMenu(x, y, items) {
786
850
  activeMenu = menu;
787
851
  requestAnimationFrame(() => {
788
852
  dismissListener = (e) => {
853
+ if (activeSubmenu?.contains(e.target))
854
+ return;
789
855
  if (activeMenu && !activeMenu.contains(e.target)) {
790
856
  closeContextMenu();
791
857
  }
@@ -801,13 +867,14 @@ function showContextMenu(x, y, items) {
801
867
  document.addEventListener("keydown", escapeListener, true);
802
868
  });
803
869
  }
804
- var activeMenu, dismissListener, escapeListener;
870
+ var activeMenu, dismissListener, escapeListener, activeSubmenu;
805
871
  var init_context_menu = __esm({
806
872
  "client/components/context-menu.js"() {
807
873
  "use strict";
808
874
  activeMenu = null;
809
875
  dismissListener = null;
810
876
  escapeListener = null;
877
+ activeSubmenu = null;
811
878
  document.addEventListener("scroll", closeContextMenu, true);
812
879
  document.addEventListener("contextmenu", () => {
813
880
  });
@@ -1060,13 +1127,13 @@ async function openAddressBook(prefillSearch) {
1060
1127
  <span class="ab-actions"></span>
1061
1128
  </div>` + items.map((c) => `
1062
1129
  <div class="ab-row" data-email="${escapeAttr(c.email)}">
1063
- <span class="ab-name">${escapeHtml(c.name || "")}</span>
1130
+ <span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">\u2022</span>' : ""}</span>
1064
1131
  <span class="ab-email">${escapeHtml(c.email)}</span>
1065
1132
  <span class="ab-source">${escapeHtml(c.source)}</span>
1066
1133
  <span class="ab-count-cell">${c.useCount || 0}</span>
1067
1134
  <span class="ab-last">${fmtDate(c.lastUsed)}</span>
1068
1135
  <span class="ab-actions">
1069
- <button type="button" class="ab-edit" title="Edit name">\u270E</button>
1136
+ <button type="button" class="ab-edit" title="Edit contact">\u270E</button>
1070
1137
  <button type="button" class="ab-del" title="Delete">\u{1F5D1}</button>
1071
1138
  <button type="button" class="ab-mail" title="Compose to">\u2709</button>
1072
1139
  </span>
@@ -1079,38 +1146,76 @@ async function openAddressBook(prefillSearch) {
1079
1146
  row.querySelector(".ab-mail")?.addEventListener("click", () => composeTo(c));
1080
1147
  });
1081
1148
  };
1082
- const beginEdit = (row, c) => {
1149
+ const beginEdit = (_row, c) => {
1083
1150
  if (editingEmail === c.email)
1084
1151
  return;
1085
1152
  editingEmail = c.email;
1086
- const nameSpan = row.querySelector(".ab-name");
1087
- const original = c.name || "";
1088
- nameSpan.innerHTML = `<input type="text" value="${escapeAttr(original)}" class="ab-name-input">`;
1089
- const inp = nameSpan.querySelector("input");
1090
- inp.focus();
1091
- inp.select();
1092
- const commit = async () => {
1093
- const newName = inp.value.trim();
1094
- if (newName !== original) {
1095
- try {
1096
- await upsertContact(newName, c.email);
1097
- c.name = newName;
1098
- } catch (e) {
1099
- alert(`Update failed: ${e?.message || e}`);
1100
- }
1101
- }
1102
- nameSpan.textContent = c.name || "";
1153
+ const back = document.createElement("div");
1154
+ back.className = "mailx-modal-backdrop";
1155
+ const card = document.createElement("div");
1156
+ card.className = "mailx-modal";
1157
+ card.innerHTML = `
1158
+ <div class="mailx-modal-title">
1159
+ <span class="mailx-modal-title-text">Edit contact</span>
1160
+ <button type="button" class="mailx-modal-close" data-role="x" title="Close (Esc)" aria-label="Close">&times;</button>
1161
+ </div>
1162
+ <div class="ab-card">
1163
+ <label class="ab-field"><span>Name</span>
1164
+ <input type="text" class="mailx-modal-input" data-field="name" value="${escapeAttr(c.name || "")}"></label>
1165
+ <label class="ab-field"><span>Email</span>
1166
+ <input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
1167
+ title="The address identifies the contact \u2014 delete and re-add to change it"></label>
1168
+ ${CONTACT_FIELDS.map((f) => `
1169
+ <label class="ab-field"><span>${escapeHtml(f.label)}</span>
1170
+ ${f.multiline ? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml(c[f.key] || "")}</textarea>` : `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
1171
+ value="${escapeAttr(c[f.key] || "")}"
1172
+ ${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
1173
+ </label>`).join("")}
1174
+ </div>
1175
+ <div class="mailx-modal-buttons">
1176
+ <span class="mailx-modal-spacer"></span>
1177
+ <button type="button" class="mailx-modal-btn" data-role="cancel">Cancel</button>
1178
+ <button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-role="save">Save</button>
1179
+ </div>`;
1180
+ back.appendChild(card);
1181
+ document.body.appendChild(back);
1182
+ card.querySelector('[data-field="name"]')?.focus();
1183
+ const shut = () => {
1184
+ back.remove();
1185
+ document.removeEventListener("keydown", onCardKey, true);
1103
1186
  editingEmail = null;
1104
1187
  };
1105
- inp.addEventListener("blur", commit);
1106
- inp.addEventListener("keydown", (e) => {
1107
- if (e.key === "Enter") {
1108
- e.preventDefault();
1109
- inp.blur();
1110
- } else if (e.key === "Escape") {
1188
+ const onCardKey = (e) => {
1189
+ if (e.key === "Escape") {
1190
+ e.stopPropagation();
1111
1191
  e.preventDefault();
1112
- inp.value = original;
1113
- inp.blur();
1192
+ shut();
1193
+ }
1194
+ };
1195
+ document.addEventListener("keydown", onCardKey, true);
1196
+ card.querySelector('[data-role="x"]')?.addEventListener("click", shut);
1197
+ card.querySelector('[data-role="cancel"]')?.addEventListener("click", shut);
1198
+ back.addEventListener("mousedown", (e) => {
1199
+ if (e.target === back)
1200
+ shut();
1201
+ });
1202
+ card.querySelector('[data-role="save"]')?.addEventListener("click", async () => {
1203
+ const val = (k) => card.querySelector(`[data-field="${k}"]`)?.value.trim() ?? "";
1204
+ const fields = {};
1205
+ for (const f of CONTACT_FIELDS)
1206
+ fields[f.key] = val(f.key);
1207
+ const newName = val("name");
1208
+ const bd = fields.birthday;
1209
+ if (bd && !/^(\d{4}|-)-\d{2}-\d{2}$/.test(bd)) {
1210
+ alert(`Birthday must be YYYY-MM-DD, or --MM-DD when the year is unknown. Got "${bd}".`);
1211
+ return;
1212
+ }
1213
+ try {
1214
+ await upsertContact(newName, c.email, fields);
1215
+ shut();
1216
+ await reload();
1217
+ } catch (e) {
1218
+ alert(`Update failed: ${e?.message || e}`);
1114
1219
  }
1115
1220
  });
1116
1221
  };
@@ -1195,17 +1300,29 @@ async function openAddressBook(prefillSearch) {
1195
1300
  });
1196
1301
  await reload();
1197
1302
  }
1303
+ function cardSummary(c) {
1304
+ return CONTACT_FIELDS.map((f) => ({ label: f.label, value: (c[f.key] || "").trim() })).filter((x) => x.value).map((x) => `${x.label}: ${x.value.replace(/\s+/g, " ")}`).join("\n");
1305
+ }
1198
1306
  function escapeHtml(s) {
1199
1307
  return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1200
1308
  }
1201
1309
  function escapeAttr(s) {
1202
1310
  return escapeHtml(s);
1203
1311
  }
1204
- var isOpen;
1312
+ var CONTACT_FIELDS, isOpen;
1205
1313
  var init_address_book = __esm({
1206
1314
  "client/components/address-book.js"() {
1207
1315
  "use strict";
1208
1316
  init_api_client();
1317
+ CONTACT_FIELDS = [
1318
+ { key: "organization", label: "Company" },
1319
+ { key: "title", label: "Job title" },
1320
+ { key: "phone", label: "Phone", type: "tel" },
1321
+ { key: "website", label: "Website", type: "url", placeholder: "https://" },
1322
+ { key: "birthday", label: "Birthday", placeholder: "YYYY-MM-DD or --MM-DD" },
1323
+ { key: "address", label: "Address", multiline: true },
1324
+ { key: "notes", label: "Notes", multiline: true }
1325
+ ];
1209
1326
  isOpen = false;
1210
1327
  }
1211
1328
  });