@rapidmx/web-client 0.7.0 → 0.9.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/apps/shared/components/admin/elevation.ts +61 -0
  3. package/apps/shared/components/admin/layout/AdminShell.tsx +73 -7
  4. package/apps/shared/components/admin/settings/BrandingForm.tsx +2 -1
  5. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +86 -24
  6. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +51 -12
  7. package/apps/shared/components/layout/BrandingChrome.tsx +3 -2
  8. package/apps/shared/components/layout/MailboxProvisioning.tsx +44 -6
  9. package/apps/shared/components/layout/UserMenu.tsx +48 -13
  10. package/apps/shared/components/mail/ConversationList.tsx +15 -6
  11. package/apps/shared/components/mail/ConversationThreadPane.tsx +2 -2
  12. package/apps/shared/components/mail/MailAddress.tsx +88 -0
  13. package/apps/shared/components/mail/MessageDetailPane.tsx +11 -7
  14. package/apps/shared/components/mail/compose/ComposeWindow.tsx +40 -4
  15. package/apps/shared/components/mail/compose/SendFailureAlert.tsx +48 -0
  16. package/apps/shared/components/mail/layout/MailShell.tsx +37 -10
  17. package/apps/shared/mail/mergeFirstPage.ts +47 -0
  18. package/apps/shared/mail/useMailLiveUpdates.ts +224 -0
  19. package/apps/www/index.tsx +65 -2
  20. package/dist/apps/shared/components/admin/elevation.d.ts +24 -0
  21. package/dist/apps/shared/components/admin/elevation.js +56 -0
  22. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +12 -4
  23. package/dist/apps/shared/components/admin/layout/AdminShell.js +49 -6
  24. package/dist/apps/shared/components/admin/settings/BrandingForm.js +1 -1
  25. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +31 -13
  26. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +5 -1
  27. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +19 -3
  28. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +3 -2
  29. package/dist/apps/shared/components/layout/BrandingChrome.js +3 -2
  30. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +5 -2
  31. package/dist/apps/shared/components/layout/MailboxProvisioning.js +42 -7
  32. package/dist/apps/shared/components/layout/UserMenu.d.ts +10 -7
  33. package/dist/apps/shared/components/layout/UserMenu.js +39 -13
  34. package/dist/apps/shared/components/mail/ConversationList.js +7 -4
  35. package/dist/apps/shared/components/mail/ConversationThreadPane.js +2 -2
  36. package/dist/apps/shared/components/mail/MailAddress.d.ts +27 -0
  37. package/dist/apps/shared/components/mail/MailAddress.js +39 -0
  38. package/dist/apps/shared/components/mail/MessageDetailPane.js +5 -3
  39. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +25 -6
  40. package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +14 -0
  41. package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +11 -0
  42. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +7 -0
  43. package/dist/apps/shared/components/mail/layout/MailShell.js +31 -11
  44. package/dist/apps/shared/mail/mergeFirstPage.d.ts +23 -0
  45. package/dist/apps/shared/mail/mergeFirstPage.js +30 -0
  46. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +48 -0
  47. package/dist/apps/shared/mail/useMailLiveUpdates.js +180 -0
  48. package/dist/apps/www/index.js +63 -2
  49. package/package.json +2 -2
@@ -19,6 +19,8 @@ import { evaluateMessageSecurity } from "@rapidmx/react-shared/crypto/messageSec
19
19
  import { useMarkMessageRead, useMessageAttachments } from "@rapidmx/react-shared/mail/mailDetailHooks.js";
20
20
  import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
21
21
  import MailShell, { useMailShell, } from "../shared/components/mail/layout/MailShell.js";
22
+ import MailAddress from "../shared/components/mail/MailAddress.js";
23
+ import { mergeFirstPage } from "../shared/mail/mergeFirstPage.js";
22
24
  import MessageDetailPane from "../shared/components/mail/MessageDetailPane.js";
23
25
  import ConversationList from "../shared/components/mail/ConversationList.js";
24
26
  import ConversationThreadPane from "../shared/components/mail/ConversationThreadPane.js";
@@ -336,7 +338,7 @@ export default function InboxPage(props) {
336
338
  return (_jsx(MailShell, { ...props, children: _jsx(InboxContent, { userUid: props.userUid }) }));
337
339
  }
338
340
  function InboxContent({ userUid }) {
339
- const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated } = useMailShell();
341
+ const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated, live } = useMailShell();
340
342
  const isMobile = useIsMobile();
341
343
  const { requestUnlock } = useUnlockPrompt();
342
344
  const [messages, setMessages] = useState([]);
@@ -870,6 +872,65 @@ function InboxContent({ userUid }) {
870
872
  mailboxFolders,
871
873
  activeMailboxUid,
872
874
  ]);
875
+ // New mail without a reload. `live` is bumped by a push event, a reconnect, the safety-net poll or the tab coming back (see
876
+ // `useMailLiveUpdates()`); this then quietly refetches the first page of whatever is listed and folds it in - unlike the
877
+ // effect above it resets nothing: not the selection, the open thread, select mode, the scroll position or the rows
878
+ // already paged in, and nothing it fetches is marked read. It stands aside for a search (whose rows aren't a folder's),
879
+ // a listing still loading and a "load more" in flight, and for events about folders the list isn't showing - a
880
+ // conversation list, which can span folders, refreshes for any. Any failure is silent: the next tick tries again.
881
+ const liveRunRef = useRef(0);
882
+ useEffect(() => {
883
+ if (live.tick === 0 || isSearching || loading || loadMoreInFlightRef.current || (!folderUid && !aggregateFolderType)) {
884
+ return;
885
+ }
886
+ if (!preferences.showAsConversations && live.folderUids) {
887
+ const shown = new Set(aggregateFolderType
888
+ ? mailboxFolders.flatMap((entry) => entry.folders.filter((f) => f.type === aggregateFolderType).map((f) => f.uid))
889
+ : [folderUid]);
890
+ if (![...live.folderUids].some((uid) => shown.has(uid))) {
891
+ return;
892
+ }
893
+ }
894
+ // Superseded by a newer refresh, or by any reload of the list itself (which bumps the search run id).
895
+ const listRun = searchRunIdRef.current;
896
+ const myRun = ++liveRunRef.current;
897
+ const isCurrent = () => searchRunIdRef.current === listRun && liveRunRef.current === myRun;
898
+ void (async () => {
899
+ try {
900
+ if (preferences.showAsConversations) {
901
+ const fresh = await listConversations(activeMailboxUid, conversationParams(0));
902
+ if (!isCurrent()) {
903
+ return;
904
+ }
905
+ const merged = mergeFirstPage(conversationsRef.current, fresh, conversationKey, MESSAGE_PAGE_SIZE);
906
+ setConversations(merged.rows);
907
+ listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
908
+ setHasMore(!merged.complete);
909
+ }
910
+ else if (aggregateFolderType) {
911
+ // No paging here (see `fetchAggregateMessages()`): the fresh merged first pages are the list.
912
+ const fresh = await fetchAggregateMessages(mailboxFolders, aggregateFolderType, effectiveFilter);
913
+ if (isCurrent()) {
914
+ setMessages(fresh);
915
+ }
916
+ }
917
+ else {
918
+ const fresh = await listMessages(folderUid, listParams);
919
+ if (!isCurrent()) {
920
+ return;
921
+ }
922
+ // A row the reader has just changed (a higher version) is not put back by a fetch that began before.
923
+ const merged = mergeFirstPage(messagesRef.current, fresh, messageUid, MESSAGE_PAGE_SIZE, (current, next) => current.version > next.version ? current : next);
924
+ setMessages(merged.rows);
925
+ listedOffsetRef.current = merged.complete ? fresh.length : listedOffsetRef.current + merged.added;
926
+ setHasMore(!merged.complete);
927
+ }
928
+ }
929
+ catch {
930
+ // Quiet by design - see above.
931
+ }
932
+ })();
933
+ }, [live.tick]);
873
934
  function handleSearchAllMail() {
874
935
  setSearchAllMailKey(searchAllMailScope);
875
936
  }
@@ -1337,7 +1398,7 @@ function InboxContent({ userUid }) {
1337
1398
  ? "bg-primary/10"
1338
1399
  : "hover:bg-surface-alt",
1339
1400
  message.flags.read ? "" : "font-semibold",
1340
- ].join(" "), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx("span", { className: "truncate", children: message.from.displayName || message.from.address }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(message.receivedDate).toLocaleDateString() })] }), aggregateFolderType && (_jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: mailboxes.find((mb) => mb.uid === message.mailboxUid)?.displayName })), isSearching && pendingUids.has(message.uid) ? (_jsxs("div", { className: "flex flex-col gap-1.5 py-0.5", children: [_jsx(Skeleton, { height: "h-3.5", className: "w-2/3 rounded-sm" }), _jsx(Skeleton, { height: "h-3", className: "w-full rounded-sm" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm truncate", children: rowSubject(message) }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: snippets[message.uid] || decryptedRows[message.uid]?.preview || message.bodyPreview }), message.hasAttachments && _jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" }), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] })] }))] })] }, message.uid))) }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus })), aggregateFolderType && (_jsx("p", { className: "p-4 text-center text-xs text-text-muted", children: "Showing the most recent mail from each mailbox. Open a specific mailbox\u2019s folder to see older mail." }))] }))] }), _jsx("div", { className: "hidden md:flex flex-1 min-w-0 min-h-0", children: preferences.showAsConversations ? (_jsx(ConversationThreadPane, { conversation: openThread?.conversation ?? null, selectedUid: openThread?.uid ?? null, mailboxUid: activeMailboxUid, folders: currentFolders, labels: mailboxLabels, onMessagePatched: patchListedMessage, onMessageRemoved: (updated) => removeListedMessage(updated.uid), onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated })) : (_jsx(MessageDetailPane, { message: selected, attachments: attachments, isSentItems: isSentItems, onRecalled: patchListedMessage, isOutbox: isOutbox, onReceiptHandled: patchListedMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: (updated) => {
1401
+ ].join(" "), children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: message.from }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(message.receivedDate).toLocaleDateString() })] }), aggregateFolderType && (_jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: mailboxes.find((mb) => mb.uid === message.mailboxUid)?.displayName })), isSearching && pendingUids.has(message.uid) ? (_jsxs("div", { className: "flex flex-col gap-1.5 py-0.5", children: [_jsx(Skeleton, { height: "h-3.5", className: "w-2/3 rounded-sm" }), _jsx(Skeleton, { height: "h-3", className: "w-full rounded-sm" })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm truncate", children: rowSubject(message) }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-text-muted font-normal", children: [_jsx("span", { className: "truncate", children: snippets[message.uid] || decryptedRows[message.uid]?.preview || message.bodyPreview }), message.hasAttachments && _jsx(HiOutlinePaperClip, { size: 12, "aria-label": "Has attachments" }), message.flags.flagged && (_jsx(HiOutlineFlag, { size: 12, "aria-label": "Flagged", className: "text-danger" }))] })] }))] })] }, message.uid))) }), hasMore && (_jsx("div", { ref: setSentinel, "data-testid": "load-more-sentinel", className: "p-4 text-center text-xs text-text-muted", children: loadMoreStatus })), aggregateFolderType && (_jsx("p", { className: "p-4 text-center text-xs text-text-muted", children: "Showing the most recent mail from each mailbox. Open a specific mailbox\u2019s folder to see older mail." }))] }))] }), _jsx("div", { className: "hidden md:flex flex-1 min-w-0 min-h-0", children: preferences.showAsConversations ? (_jsx(ConversationThreadPane, { conversation: openThread?.conversation ?? null, selectedUid: openThread?.uid ?? null, mailboxUid: activeMailboxUid, folders: currentFolders, labels: mailboxLabels, onMessagePatched: patchListedMessage, onMessageRemoved: (updated) => removeListedMessage(updated.uid), onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated })) : (_jsx(MessageDetailPane, { message: selected, attachments: attachments, isSentItems: isSentItems, onRecalled: patchListedMessage, isOutbox: isOutbox, onReceiptHandled: patchListedMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: (updated) => {
1341
1402
  // Same reasoning as onArchived below - a move takes the message out of the folder
1342
1403
  // being listed, so it leaves the list rather than being patched in place.
1343
1404
  removeListedMessage(updated.uid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/web-client",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "RapidMX's webmail and admin console React UI for web-based clients.",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/rapidmx/web-client",
@@ -39,7 +39,7 @@
39
39
  "@dnd-kit/core": "^6.3.1",
40
40
  "@journeyapps/wa-sqlite": "^2.0.4",
41
41
  "@peculiar/x509": "^2.1.0",
42
- "@rapidmx/react-shared": "^0.7.0",
42
+ "@rapidmx/react-shared": "^0.9.0",
43
43
  "@tiptap/core": "^3.31.3",
44
44
  "@tiptap/extension-highlight": "^3.31.3",
45
45
  "@tiptap/extension-image": "^3.31.3",