@rapidmx/web-client 0.16.0 → 0.17.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/README.md +36 -28
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +3 -1
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +3 -1
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +3 -1
- package/apps/admin/mailboxes/[uid].tsx +3 -1
- package/apps/admin/mailboxes/new/index.tsx +3 -1
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +4 -1
- package/apps/shared/components/admin/setup/SetupWizard.tsx +3 -1
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +2 -2
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +3 -2
- package/apps/shared/components/layout/AppShell.tsx +24 -43
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -0
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +19 -15
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +202 -4
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +37 -34
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +4 -2
- package/apps/shared/components/tasks/layout/TasksShell.tsx +3 -2
- package/apps/shared/keyboard/GlobalShortcuts.tsx +1 -1
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +5 -2
- package/apps/www/contacts/[uid].tsx +6 -3
- package/apps/www/contacts/index.tsx +8 -3
- package/apps/www/index.tsx +146 -31
- package/apps/www/messages/[uid].tsx +8 -4
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +5 -2
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +5 -2
- package/apps/www/settings/filters/[uid].tsx +5 -2
- package/apps/www/settings/filters/index.tsx +5 -2
- package/apps/www/settings/filters/new/index.tsx +30 -5
- package/apps/www/settings/labels/index.tsx +5 -2
- package/apps/www/settings/privacy/index.tsx +5 -2
- package/apps/www/settings/profile/index.tsx +5 -2
- package/apps/www/settings/read-receipts/index.tsx +5 -2
- package/apps/www/settings/sharing/index.tsx +5 -2
- package/apps/www/settings/signatures/[uid].tsx +5 -2
- package/apps/www/settings/signatures/index.tsx +5 -2
- package/apps/www/settings/signatures/new/index.tsx +6 -3
- package/apps/www/tasks/index.tsx +7 -2
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +3 -1
- package/dist/apps/admin/mailboxes/[uid].js +3 -1
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +4 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +3 -2
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +9 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +10 -12
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +13 -9
- package/dist/apps/shared/components/mail/layout/MailShell.js +27 -33
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -2
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +4 -2
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +6 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +117 -19
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useCallback, useMemo, useState } from "react";
|
|
7
|
+
import { useNavigationEffects, useRouter } from "@rapidrest/react/client";
|
|
8
|
+
import { AppChrome } from "../shared/components/layout/AppShell.js";
|
|
9
|
+
import { AppFrameContext } from "../shared/navigation/frameContext.js";
|
|
10
|
+
import { titleWithUnread } from "../shared/mail/useUnreadTitle.js";
|
|
11
|
+
/**
|
|
12
|
+
* The app-rail entry (or `"settings"`, which has none) that a route highlights: the first segment of its template names the
|
|
13
|
+
* app. Anything else - Mail's `/` and `/messages/:uid` - is Mail.
|
|
14
|
+
*/
|
|
15
|
+
export function activeAppOf(route) {
|
|
16
|
+
const app = route.split("/")[1];
|
|
17
|
+
return app === "calendar" || app === "contacts" || app === "tasks" || app === "settings" ? app : "mail";
|
|
18
|
+
}
|
|
19
|
+
export default function WwwShell({ children, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, branding, appearance, }) {
|
|
20
|
+
const { route, pending } = useRouter();
|
|
21
|
+
const [takeovers, setTakeovers] = useState(0);
|
|
22
|
+
const enterTakeover = useCallback(() => {
|
|
23
|
+
setTakeovers((n) => n + 1);
|
|
24
|
+
return () => setTakeovers((n) => n - 1);
|
|
25
|
+
}, []);
|
|
26
|
+
const frame = useMemo(() => ({ enterTakeover }), [enterTakeover]);
|
|
27
|
+
// The content region is what a page load would have started at: focus it (it is only focusable by script), start at the top, and say the
|
|
28
|
+
// page's title - without the unread count that the tab's title carries in front of it.
|
|
29
|
+
useNavigationEffects({ focus: "#app-content", scroll: "top", announce: ({ title }) => titleWithUnread(title, 0) });
|
|
30
|
+
return (_jsx(AppFrameContext.Provider, { value: frame, children: _jsx(AppChrome, { active: activeAppOf(route), busy: pending, hideChrome: takeovers > 0, userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, branding: branding, appearance: appearance, children: children }) }));
|
|
31
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { CalendarShellProps } from "../../shared/components/calendar/layout/CalendarShell.js";
|
|
2
|
-
declare
|
|
3
|
-
export default
|
|
3
|
+
declare function CalendarPage(props: CalendarShellProps): React.JSX.Element;
|
|
4
|
+
export default CalendarPage;
|
|
5
|
+
/** The tab's title: `Brand: Calendar` (see `pageTitle()`). */
|
|
6
|
+
export declare const title: (props: {
|
|
7
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
8
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
9
9
|
import { HiOutlineBars3, HiOutlinePlus } from "react-icons/hi2";
|
|
@@ -346,4 +346,6 @@ function CalendarContent({ userUid, bookingHref }) {
|
|
|
346
346
|
const sidebarContent = (_jsxs(_Fragment, { children: [_jsx(MiniDatePicker, { selected: viewDate, onSelect: (date) => setViewDate(startOfDay(date)) }), _jsx(CalendarListSidebar, { mailboxCalendars: mailboxCalendars, checkedFolderUids: checkedFolderUids, onToggle: toggleCalendar, onAddCalendar: handleAddCalendar, colorFor: colorFor })] }));
|
|
347
347
|
return (_jsxs("div", { className: "flex-1 min-w-0 flex min-h-0", children: [_jsx("div", { className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col overflow-y-auto", children: sidebarContent }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Calendars", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "lg:hidden w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text shrink-0", "aria-label": "Open calendars", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), !isMobile && (_jsx(Button, { type: "button", onClick: (e) => openNewEvent(undefined, undefined, undefined, anchorOf(e.currentTarget, "below")), className: "!w-auto shrink-0", ...newEventHint, children: "+ New event" })), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", ...previousHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, ...todayHint, className: "px-2 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "Today" }), _jsx("button", { type: "button", onClick: () => shiftView(1), "aria-label": "Next", ...nextHint, className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u203A" })] }), _jsx("h1", { className: "hidden md:block text-lg font-bold tracking-tight", children: title }), _jsx("div", { className: "ml-auto flex flex-wrap gap-1", children: VIEW_TYPES.map((v) => (_jsx(ViewButton, { view: v, current: view, onSelect: setView }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), _jsx("div", { ref: swipe.ref, className: "flex-1 min-w-0 flex flex-col min-h-0 overflow-hidden", ...swipe.handlers, style: swipe.style, children: _jsx("div", { "data-swipe-phase": swipe.phase, className: "flex-1 min-w-0 flex flex-col min-h-0", style: swipe.contentStyle, children: _jsx("div", { className: "flex-1 min-w-0 flex flex-col min-h-0", style: wheelSlide.style, children: loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragStart: () => setDragging(true), onDragEnd: handleDragEnd, onDragCancel: () => setDragging(false), children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor, true) })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid, anchor) => openNewEvent(start, end, targetFolderUid, anchor) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: (start, end, anchor) => openNewEvent(start, end, undefined, anchor) })) })) }) }) }), modal && modalMailbox && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid: modalMailbox.uid, folderUid: modalFolderUid, calendars: calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name })), mailboxOptions: mailboxOptions, folderColors: folderColors, organizerAddress: modalMailbox.primarySmtpAddress, organizerAliases: modalMailbox.aliasAddresses, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, initialAllDay: modal.initialAllDay, anchor: modal.anchor, bookingHref: bookingHref, onSaved: handleSaved, onDeleted: handleDeleted })), isMobile && mailboxUid && folderUid && !modal && _jsx(FloatingActionButton, { label: "New event", icon: HiOutlinePlus, onClick: () => openNewEvent() })] })] }));
|
|
348
348
|
}
|
|
349
|
-
export default
|
|
349
|
+
export default CalendarPage;
|
|
350
|
+
/** The tab's title: `Brand: Calendar` (see `pageTitle()`). */
|
|
351
|
+
export const title = pageTitle("Calendar");
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/contacts/index.tsx` keeps its
|
|
5
|
+
* existing inline detail pane and never navigates here; see that file's `handleSelectRow`. There is no
|
|
6
|
+
* equivalent "new contact" route — an unsaved contact has no uid to route on, so creation stays an
|
|
7
|
+
* in-place mode-switch on every device (see `apps/www/contacts/index.tsx`'s `handleNew`).
|
|
8
|
+
*/
|
|
9
|
+
declare function ContactDetailPage(props: ContactsShellProps & {
|
|
3
10
|
params: {
|
|
4
11
|
uid: string;
|
|
5
12
|
};
|
|
6
|
-
}
|
|
7
|
-
export default
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
export default ContactDetailPage;
|
|
15
|
+
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
16
|
+
export declare const title: (props: {
|
|
17
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
18
|
+
}) => string;
|
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
7
|
-
import { useNavigate } from "../../shared/navigation/
|
|
6
|
+
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
|
+
import { useNavigate } from "../../shared/navigation/index.js";
|
|
8
8
|
import { useEffect, useState } from "react";
|
|
9
9
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
10
10
|
import { deleteContact, getContact } from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
@@ -72,4 +72,6 @@ function ContactDetailContent({ uid }) {
|
|
|
72
72
|
}
|
|
73
73
|
return (_jsx("div", { className: "p-6", children: _jsx(ContactDetailPane, { contact: contact, onEdit: () => setMode("edit"), onDelete: () => handleDelete(contact), backHref: backHref, onKeysChanged: handleKeysChanged }) }));
|
|
74
74
|
}
|
|
75
|
-
export default
|
|
75
|
+
export default ContactDetailPage;
|
|
76
|
+
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
77
|
+
export const title = pageTitle("Contacts");
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { ContactsShellProps } from "../../shared/components/contacts/layout/ContactsShell.js";
|
|
2
|
-
declare
|
|
3
|
-
export default
|
|
3
|
+
declare function ContactsPage(props: ContactsShellProps): React.JSX.Element;
|
|
4
|
+
export default ContactsPage;
|
|
5
|
+
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
6
|
+
export declare const title: (props: {
|
|
7
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
8
|
+
}) => string;
|
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
7
|
-
import { useNavigate } from "../../shared/navigation/
|
|
6
|
+
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
|
+
import { useNavigate } from "../../shared/navigation/index.js";
|
|
8
8
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
10
10
|
import { createContact, deleteContact, listContacts, listDeletedContacts, setContactFavorite, updateContact, } from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
@@ -344,6 +344,8 @@ function ContactsContent({ userUid }) {
|
|
|
344
344
|
return (_jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex flex-col lg:flex-row md:flex-none md:h-[calc(100dvh_-_var(--rr-header-h,4rem))] md:overflow-hidden", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView, showDeleted: canViewDeleted }), _jsxs("div", { className: "flex-1 min-w-0 min-h-0 flex", children: [_jsxs("div", { className: [
|
|
345
345
|
"w-full md:w-[clamp(16rem,32vw,26rem)] min-w-0 md:shrink-0 md:border-r border-border md:flex flex-col min-h-0",
|
|
346
346
|
mode === "new" ? "hidden md:flex" : "flex",
|
|
347
|
-
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), onKeysChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
|
|
347
|
+
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: () => setConfirmingBulkDelete(true), onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile, shortcuts: true, hideNew: isMobile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { ref: searchInputRef, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search contacts", "aria-label": "Search contacts", className: INPUT_CLASS }) }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), (isDeletedView ? deletedTruncated : truncated) && (_jsx("div", { className: "p-3", children: _jsxs(Alert, { children: ["This folder has more contacts than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] }) })), deletedError && isDeletedView && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: deletedError }) })), (isDeletedView ? deletedLoading : loading) ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : sorted.length === 0 ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "No contacts found." })) : (_jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: _jsxs("table", { className: "w-full table-fixed text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-10 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "w-[58%] px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Name", sortColumn === "name" ? (sortDesc ? " ▾" : " ▴") : ""] }) }), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("info"), className: "block max-w-full truncate text-xs font-bold uppercase tracking-wide text-text-muted", children: ["Contact info", sortColumn === "info" ? (sortDesc ? " ▾" : " ▴") : ""] }) })] }) }), _jsx("tbody", { children: sorted.map((contact) => (_jsxs("tr", { className: ["border-b border-border", contact.uid === selectedUid ? "bg-primary/10" : "hover:bg-surface-alt"].join(" "), children: [!isDeletedView && (_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${contact.displayName}`, checked: checkedUids.has(contact.uid), onChange: () => toggleChecked(contact.uid) }) })), _jsx("td", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSelectRow(contact), className: "flex w-full min-w-0 items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "min-w-0 truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "max-w-0 truncate px-3 py-2 text-text-muted", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 min-h-0 flex-col md:flex", mode === "new" ? "flex" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "m-6 mb-0 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: [savedElsewhere.displayName, " was added to", " ", mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s contacts" })] })), mode === "new" && mailboxUid && folderUid ? (_jsx(ContactForm, { mailboxUid: mailboxUid, folderUid: folderUid, mailboxes: writableMailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-6", children: _jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected), onKeysChanged: () => void reload(), canResolveKeys: ownsMailbox || delegateCanUpdate }) })) : (_jsx("div", { className: "flex flex-1 items-center justify-center p-6 text-center", children: _jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }) }))] })] }), _jsxs(Modal, { open: confirmingBulkDelete, onClose: () => setConfirmingBulkDelete(false), title: "Delete contacts", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Delete ", checkedContacts.length, " selected ", checkedContacts.length === 1 ? "contact" : "contacts", "? They move to Deleted contacts."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setConfirmingBulkDelete(false), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleBulkDelete, children: "Delete" })] })] }), isMobile && mode !== "new" && _jsx(FloatingActionButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: handleNew })] }));
|
|
348
348
|
}
|
|
349
|
-
export default
|
|
349
|
+
export default ContactsPage;
|
|
350
|
+
/** The tab's title: `Brand: Contacts` (see `pageTitle()`). */
|
|
351
|
+
export const title = pageTitle("Contacts");
|
package/dist/apps/www/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { MailShellProps } from "../shared/components/mail/layout/MailShell.js";
|
|
2
|
-
declare
|
|
3
|
-
export default
|
|
3
|
+
declare function InboxPage(props: MailShellProps): React.JSX.Element;
|
|
4
|
+
export default InboxPage;
|
|
5
|
+
/** The tab's title: `Brand: Mail` (see `pageTitle()`). */
|
|
6
|
+
export declare const title: (props: {
|
|
7
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
8
|
+
}) => string;
|
package/dist/apps/www/index.js
CHANGED
|
@@ -3,9 +3,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
7
|
-
import { useNavigate } from "../shared/navigation/
|
|
8
|
-
import { whenIdle } from "
|
|
6
|
+
import { pageTitle } from "../shared/navigation/pageTitle.js";
|
|
7
|
+
import { useNavigate } from "../shared/navigation/index.js";
|
|
8
|
+
import { whenIdle } from "@rapidrest/react/client";
|
|
9
9
|
import { prefetchComposeWindow } from "../shared/components/mail/compose/ComposeContext.js";
|
|
10
10
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
11
11
|
import { createPortal } from "react-dom";
|
|
@@ -38,6 +38,10 @@ import MoveToFolderDialog from "../shared/components/mail/MoveToFolderDialog.js"
|
|
|
38
38
|
import { EncryptedPreview } from "../shared/components/mail/reading/EncryptedPreview.js";
|
|
39
39
|
import MailListToolbar from "../shared/components/mail/MailListToolbar.js";
|
|
40
40
|
import MailSelectionBar from "../shared/components/mail/MailSelectionBar.js";
|
|
41
|
+
import EmptyFolderBar from "../shared/components/mail/EmptyFolderBar.js";
|
|
42
|
+
import { EMPTIABLE_FOLDER_TYPES } from "../shared/mail/permanentDelete.js";
|
|
43
|
+
import { usePermanentDelete } from "../shared/mail/usePermanentDelete.js";
|
|
44
|
+
import { useMailboxUpdateAccess } from "../shared/mail/useMailboxUpdateAccess.js";
|
|
41
45
|
import { CONVERSATION_SORT_NOTE, CONVERSATION_SORT_UNAVAILABLE, MAIL_LIST_CLASSIFICATION_FILTERS, getMailListPreferences, setMailListPreferences, sortConversations, } from "../shared/components/mail/listPreferences.js";
|
|
42
46
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
43
47
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
@@ -365,7 +369,7 @@ function InboxPage(props) {
|
|
|
365
369
|
return (_jsx(MailShell, { ...props, children: _jsx(InboxContent, { userUid: props.userUid }) }));
|
|
366
370
|
}
|
|
367
371
|
function InboxContent({ userUid }) {
|
|
368
|
-
const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot } = useMailShell();
|
|
372
|
+
const { folderUid, mailboxUid, mailboxes, mailboxFolders, aggregateFolderType, onFolderCreated, noteFolderUids, live, trackMessageChange, folderCountOf, mobileSearchSlot } = useMailShell();
|
|
369
373
|
const isMobile = useIsMobile();
|
|
370
374
|
const navigate = useNavigate();
|
|
371
375
|
const { requestUnlock } = useUnlockPrompt();
|
|
@@ -408,6 +412,8 @@ function InboxContent({ userUid }) {
|
|
|
408
412
|
// or it would act on a selection that is still arriving.
|
|
409
413
|
const [resolvingSelection, setResolvingSelection] = useState(0);
|
|
410
414
|
const [bulkBusy, setBulkBusy] = useState(false);
|
|
415
|
+
// Delete on what is already in Deleted Items, and Empty folder: confirmed, then done for good (see `usePermanentDelete()`).
|
|
416
|
+
const permanent = usePermanentDelete();
|
|
411
417
|
// Bumped to force the list effect below to re-run - a bulk update is deliberately neither atomic nor
|
|
412
418
|
// all-or-nothing (see `bulkUpdateMessages()`), so a rejection means refetching rather than guessing
|
|
413
419
|
// which half of the selection actually landed.
|
|
@@ -467,6 +473,8 @@ function InboxContent({ userUid }) {
|
|
|
467
473
|
// Always set once this page renders: a caller with no mailbox at all gets `MailboxProvisioning` from the shell instead.
|
|
468
474
|
const activeMailboxUid = (mailboxUid ?? primaryMailboxUid(mailboxes, userUid));
|
|
469
475
|
const mailboxKeys = mailboxes.find((mb) => mb.uid === activeMailboxUid)?.keys ?? [];
|
|
476
|
+
// False for a mailbox shared with the reader view-only: Empty folder is not offered there (the server would refuse it).
|
|
477
|
+
const mailboxWritable = useMailboxUpdateAccess(mailboxes.find((mb) => mb.uid === activeMailboxUid));
|
|
470
478
|
// The Sort/Filter menus' and "Show as conversations"' current settings, remembered per mailbox across
|
|
471
479
|
// reloads (`listPreferences.ts`). Read during render, not in an effect, so the very first listing
|
|
472
480
|
// already uses the remembered arrangement rather than fetching the default one and immediately
|
|
@@ -1744,6 +1752,49 @@ function InboxContent({ userUid }) {
|
|
|
1744
1752
|
function moveSelectionToType(type, name, chosen) {
|
|
1745
1753
|
return runBulkAction((moving) => moveToFolderOfType(moving, type, name), true, chosen);
|
|
1746
1754
|
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Permanently deletes `chosen` - messages that are already in Deleted Items - once the reader has confirmed it (`usePermanentDelete()`
|
|
1757
|
+
* asks, deletes, tells the badges and says what happened). The rows that went leave the list, and selection and advance behave as for
|
|
1758
|
+
* Archive; a conversation list is reloaded, as after every bulk action on it, and so is a flat one when the server refused some
|
|
1759
|
+
* (those stay listed, and the reload shows what is really there). Resolves whether anything was deleted.
|
|
1760
|
+
*/
|
|
1761
|
+
async function purgeChosen(chosen) {
|
|
1762
|
+
const outcome = await permanent.requestPermanentDelete(chosen);
|
|
1763
|
+
if (!outcome) {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
if (asConversations) {
|
|
1767
|
+
setSelectedConversationIds(new Set());
|
|
1768
|
+
setConversationMessagesById({});
|
|
1769
|
+
setRefreshKey((n) => n + 1);
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
removeListedMessages(new Set(outcome.deleted.map((m) => m.uid)));
|
|
1773
|
+
if (outcome.failed.length > 0) {
|
|
1774
|
+
setRefreshKey((n) => n + 1);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
setSelectedUids(new Set());
|
|
1778
|
+
return outcome.deleted.length > 0;
|
|
1779
|
+
}
|
|
1780
|
+
/** Empty folder: permanently deletes everything in the folder being listed (Deleted Items or Junk Email) after the reader has confirmed. */
|
|
1781
|
+
async function emptyListedFolder(folder, count) {
|
|
1782
|
+
const outcome = await permanent.requestEmptyFolder({ uid: folder.uid, name: folder.name }, count);
|
|
1783
|
+
if (!outcome) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (outcome.emptied) {
|
|
1787
|
+
setMessages([]);
|
|
1788
|
+
setConversations([]);
|
|
1789
|
+
}
|
|
1790
|
+
setSelectedUid(null);
|
|
1791
|
+
setOpenThread(null);
|
|
1792
|
+
setSelectedUids(new Set());
|
|
1793
|
+
setSelectedConversationIds(new Set());
|
|
1794
|
+
setConversationMessagesById({});
|
|
1795
|
+
// What is really left - after a partial delete, or one that failed outright - is whatever the server lists.
|
|
1796
|
+
setRefreshKey((n) => n + 1);
|
|
1797
|
+
}
|
|
1747
1798
|
// ---- Swiping a row on a phone (see `SwipeRow`): right to left archives, left to right asks for a folder. Both go through the same
|
|
1748
1799
|
// ---- bulk path as the selection bar's Archive and Move to, so the badges, the optimistic bookkeeping and the failure pop-up are one
|
|
1749
1800
|
// ---- implementation. Not offered where the actions aren't (an aggregate view spans mailboxes, search results are a mixed bag, the
|
|
@@ -1864,6 +1915,10 @@ function InboxContent({ userUid }) {
|
|
|
1864
1915
|
function folderTypeOf(folderUidOfMessage) {
|
|
1865
1916
|
return mailboxFolders.flatMap((mf) => mf.folders).find((f) => f.uid === folderUidOfMessage)?.type;
|
|
1866
1917
|
}
|
|
1918
|
+
/** Whether a message is in its mailbox's Deleted Items - where Delete is the permanent one. */
|
|
1919
|
+
function inDeletedItems(message) {
|
|
1920
|
+
return folderTypeOf(message.folderUid) === "deleted_items";
|
|
1921
|
+
}
|
|
1867
1922
|
/** Moves the keyboard's focus to a row - and scrolls it into view - once the selection has moved to it. */
|
|
1868
1923
|
function focusRow(uid) {
|
|
1869
1924
|
// Only called from a key press, when the list is on screen.
|
|
@@ -1921,13 +1976,13 @@ function InboxContent({ userUid }) {
|
|
|
1921
1976
|
return all.filter((m) => !folderUid || crossMailbox || m.folderUid === folderUid);
|
|
1922
1977
|
}
|
|
1923
1978
|
/**
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1979
|
+
* What the keyboard acts on - the ticked rows in select mode, else the open message or conversation - keeping only the messages `applies`
|
|
1980
|
+
* accepts (Mark read leaves out what is already read: nothing to do then). Nothing (an empty list) while another bulk action or a
|
|
1981
|
+
* permanent delete is on the wire, like the bar's buttons, or when the open conversation could not be loaded.
|
|
1927
1982
|
*/
|
|
1928
|
-
async function
|
|
1929
|
-
if (bulkBusy || resolvingSelection > 0) {
|
|
1930
|
-
return;
|
|
1983
|
+
async function keyboardTargets(applies = () => true) {
|
|
1984
|
+
if (bulkBusy || resolvingSelection > 0 || permanent.busy) {
|
|
1985
|
+
return [];
|
|
1931
1986
|
}
|
|
1932
1987
|
let chosen;
|
|
1933
1988
|
try {
|
|
@@ -1939,23 +1994,48 @@ function InboxContent({ userUid }) {
|
|
|
1939
1994
|
}
|
|
1940
1995
|
catch (err) {
|
|
1941
1996
|
notifyApiError(err, "Couldn't load the messages in that conversation");
|
|
1942
|
-
return;
|
|
1997
|
+
return [];
|
|
1943
1998
|
}
|
|
1944
|
-
|
|
1999
|
+
return chosen.filter(applies);
|
|
2000
|
+
}
|
|
2001
|
+
/** Closes the open thread (and the selection in it) - what a delete of the whole conversation leaves nothing to show of. */
|
|
2002
|
+
function closeOpenThread() {
|
|
2003
|
+
setOpenThread(null);
|
|
2004
|
+
setSelectedUid(null);
|
|
2005
|
+
}
|
|
2006
|
+
/** Runs a bulk action on `chosen` (from `keyboardTargets()`), closing the open thread after a move away from it. */
|
|
2007
|
+
async function actOnKeyboardTargets(chosen, action, removesRows) {
|
|
1945
2008
|
if (chosen.length === 0) {
|
|
1946
2009
|
return;
|
|
1947
2010
|
}
|
|
1948
2011
|
const succeeded = await runBulkAction(action, removesRows, chosen);
|
|
1949
2012
|
if (succeeded && removesRows && threadPane && !selectMode) {
|
|
1950
|
-
|
|
1951
|
-
|
|
2013
|
+
closeOpenThread();
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
/** Runs a bulk action on what the keyboard acts on (`keyboardTargets()`). */
|
|
2017
|
+
async function runOnKeyboardTargets(action, removesRows, applies = () => true) {
|
|
2018
|
+
await actOnKeyboardTargets(await keyboardTargets(applies), action, removesRows);
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Delete, from the keyboard: what is in Deleted Items is permanently deleted (after a confirmation), anything else moves to its mailbox's
|
|
2022
|
+
* Deleted Items - as the selection bar's Delete does. A mix (search results from several folders) moves only what is not in Deleted Items yet.
|
|
2023
|
+
*/
|
|
2024
|
+
async function deleteFromKeyboard() {
|
|
2025
|
+
const targets = await keyboardTargets();
|
|
2026
|
+
if (targets.length > 0 && targets.every(inDeletedItems)) {
|
|
2027
|
+
if ((await purgeChosen(targets)) && threadPane && !selectMode) {
|
|
2028
|
+
closeOpenThread();
|
|
2029
|
+
}
|
|
2030
|
+
return;
|
|
1952
2031
|
}
|
|
2032
|
+
await actOnKeyboardTargets(targets.filter((message) => !inDeletedItems(message)), (chosen) => moveToFolderOfType(chosen, "deleted_items", "Deleted Items"), true);
|
|
1953
2033
|
}
|
|
1954
2034
|
useShortcut(SHORTCUTS.mail.next, () => selectNeighbour(1), { enabled: canMoveSelection });
|
|
1955
2035
|
useShortcut(SHORTCUTS.mail.previous, () => selectNeighbour(-1), { enabled: canMoveSelection });
|
|
1956
2036
|
useShortcut(SHORTCUTS.mail.nextUnread, () => selectNextUnread(1), { enabled: canMoveSelection });
|
|
1957
2037
|
useShortcut(SHORTCUTS.mail.previousUnread, () => selectNextUnread(-1), { enabled: canMoveSelection });
|
|
1958
|
-
useShortcut(SHORTCUTS.mail.delete, () => void
|
|
2038
|
+
useShortcut(SHORTCUTS.mail.delete, () => void deleteFromKeyboard(), { enabled: keyboardActions && keyboardTargetExists });
|
|
1959
2039
|
useShortcut(SHORTCUTS.mail.markRead, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, true, { patch: patchListedMessage, track: trackMessageChange }), false, isUnread), { enabled: keyboardActions && keyboardTargetExists });
|
|
1960
2040
|
useShortcut(SHORTCUTS.mail.markUnread, () => void runOnKeyboardTargets((chosen) => setReadStateMany(chosen, false, { patch: patchListedMessage, track: trackMessageChange }), false, (message) => !isUnread(message)), { enabled: keyboardActions && keyboardTargetExists });
|
|
1961
2041
|
useShortcut(SHORTCUTS.mail.flag, () =>
|
|
@@ -1995,6 +2075,19 @@ function InboxContent({ userUid }) {
|
|
|
1995
2075
|
const labelsDisabledReason = crossMailbox && selectedMessages.some((m) => m.mailboxUid !== activeMailboxUid)
|
|
1996
2076
|
? "Labels can only be applied here to messages in the open mailbox"
|
|
1997
2077
|
: undefined;
|
|
2078
|
+
// Delete is the permanent one for what is in Deleted Items: for the selection when there is one (search results can come from any folder),
|
|
2079
|
+
// else for the folder being viewed, so the button says so before anything is ticked.
|
|
2080
|
+
const deletesPermanently = selectedMessages.length > 0 ? selectedMessages.every(inDeletedItems) : currentFolders.find((f) => f.uid === folderUid)?.type === "deleted_items";
|
|
2081
|
+
// Empty folder is offered at the top of the list of a mailbox's own Deleted Items or Junk Email - not for search results, nor for the "All
|
|
2082
|
+
// mailboxes" views, which have no one folder to empty.
|
|
2083
|
+
const emptiableFolder = !isSearching && !aggregateFolderType ? currentFolders.find((f) => f.uid === folderUid && EMPTIABLE_FOLDER_TYPES.includes(f.type)) : undefined;
|
|
2084
|
+
// How many it holds as the sidebar's counts have it (kept right as messages leave), unless that says none while the list shows some.
|
|
2085
|
+
const emptiableCount = emptiableFolder && folderCountOf(emptiableFolder).total > 0 ? folderCountOf(emptiableFolder).total : undefined;
|
|
2086
|
+
const emptyDisabledReason = !mailboxWritable
|
|
2087
|
+
? "This mailbox is shared with you view-only"
|
|
2088
|
+
: listedRowCount === 0 && emptiableCount === undefined
|
|
2089
|
+
? "This folder is already empty"
|
|
2090
|
+
: undefined;
|
|
1998
2091
|
if (!folderUid && !aggregateFolderType) {
|
|
1999
2092
|
// `MailShell` never renders this component at all until a mailbox is resolved (see its own
|
|
2000
2093
|
// full-screen `MailboxProvisioning` takeover otherwise) — this is purely the brief gap before
|
|
@@ -2014,7 +2107,10 @@ function InboxContent({ userUid }) {
|
|
|
2014
2107
|
// Optimistic: the rows and the folder badges change now, and change back if the server refuses.
|
|
2015
2108
|
(chosen) => setReadStateMany(chosen, read, { patch: patchListedMessage, track: trackMessageChange }), false), onSetFlagged: (flagged) => void runBulkAction((chosen) => setMessagesFlagged(chosen, flagged), false), shortcuts: keyboardActions, onArchive: () => void runBulkAction(bulkArchive, true), onMoveTo: async (targetFolderUid) => {
|
|
2016
2109
|
await runBulkAction((chosen) => moveMessages(chosen, targetFolderUid), true);
|
|
2017
|
-
}, onReportJunk: () => void moveSelectionToType("junk", "Junk Email"), onDelete: () => void
|
|
2110
|
+
}, onReportJunk: () => void moveSelectionToType("junk", "Junk Email"), onDelete: () => void (deletesPermanently
|
|
2111
|
+
? purgeChosen(selectedMessages)
|
|
2112
|
+
: // A mix (search results from several folders) moves only what is not in Deleted Items yet.
|
|
2113
|
+
moveSelectionToType("deleted_items", "Deleted Items", selectedMessages.filter((m) => !inDeletedItems(m)))), deletesPermanently: deletesPermanently, busy: bulkBusy || resolvingSelection > 0 || permanent.busy })) : (_jsx(MailListToolbar, { sortBy: preferences.sortBy, sortOrder: preferences.sortOrder, filter: preferences.filter, labelUids: preferences.labelUids, labels: mailboxLabels, mailboxUid: activeMailboxUid, onLabelCreated: (label) => setMailboxLabels((prev) => [...prev, label]), onLabelUidsChange: (nextLabelUids) => updatePreferences({ labelUids: nextLabelUids }), showAsConversations: preferences.showAsConversations, onSortChange: (sortBy, sortOrder) => updatePreferences({ sortBy, sortOrder }), onFilterChange: (filter) => updatePreferences({ filter }), onShowAsConversationsChange: (showAsConversations) => updatePreferences({ showAsConversations }), selectMode: selectMode, onSelectModeChange: setSelectMode, offerClassificationFilters: offerClassificationFilters, filterDisabled: isSearching, filterDisabledReason: "Filters don't apply to search results", sortKeysDisabled: isSearching || !!aggregateFolderType,
|
|
2018
2114
|
// A conversation row is a thread summary, so only some of the keys have anything to
|
|
2019
2115
|
// order by - the rest stay pickable and are applied to the rows already fetched.
|
|
2020
2116
|
unavailableSortKeys: asConversations ? CONVERSATION_SORT_UNAVAILABLE : undefined, sortKeysNote: isSearching
|
|
@@ -2027,7 +2123,7 @@ function InboxContent({ userUid }) {
|
|
|
2027
2123
|
? "Open a mailbox's own folder to select messages"
|
|
2028
2124
|
: loading
|
|
2029
2125
|
? "Wait for this folder to finish loading"
|
|
2030
|
-
: "There is nothing here to select" })), !isMobile && _jsx("div", { className: "p-2 border-b border-border", children: searchField }), isMobile && mobileSearchSlot && createPortal(searchField, mobileSearchSlot), offerClassificationFilters && (_jsx("div", { className: "flex border-b border-border text-xs", children: MAIL_LIST_CLASSIFICATION_FILTERS.map(({ value, label }) => (_jsx("button", { type: "button", "aria-pressed": preferences.filter === value, onClick: () => updatePreferences({ filter: value }), className: [
|
|
2126
|
+
: "There is nothing here to select" })), !isMobile && _jsx("div", { className: "p-2 border-b border-border", children: searchField }), isMobile && mobileSearchSlot && createPortal(searchField, mobileSearchSlot), emptiableFolder && !selectMode && (_jsx(EmptyFolderBar, { folderName: emptiableFolder.name, count: emptiableCount, disabled: !!emptyDisabledReason || permanent.busy || bulkBusy, disabledReason: emptyDisabledReason, onEmpty: () => void emptyListedFolder(emptiableFolder, emptiableCount) })), permanent.dialog, offerClassificationFilters && (_jsx("div", { className: "flex border-b border-border text-xs", children: MAIL_LIST_CLASSIFICATION_FILTERS.map(({ value, label }) => (_jsx("button", { type: "button", "aria-pressed": preferences.filter === value, onClick: () => updatePreferences({ filter: value }), className: [
|
|
2031
2127
|
"flex-1 py-1.5 font-semibold",
|
|
2032
2128
|
preferences.filter === value
|
|
2033
2129
|
? "text-primary-dark border-b-2 border-primary-dark"
|
|
@@ -2062,6 +2158,8 @@ function InboxContent({ userUid }) {
|
|
|
2062
2158
|
// whichever folder is currently being viewed (into Archive), so it's removed
|
|
2063
2159
|
// from the list rather than patched in place.
|
|
2064
2160
|
removeListedMessage(updated.uid);
|
|
2065
|
-
}, labels: labels, onLabelsChanged: patchListedMessage, onLabelCreated: (label) => (otherMailbox ? setOtherMailboxLabels : setMailboxLabels)((prev) => [...prev, label]) })) }), _jsx(MoveToFolderDialog, { open: moveDialog !== null, onClose: () => setMoveDialog(null), mailboxUid: activeMailboxUid, folders: currentFolders, currentFolderUid: folderUid, count: moveDialog === null ? 1 : "messages" in moveDialog ? moveDialog.messages.length : moveDialog.conversation.messageCount, onMove: swipeMove, onFolderCreated: onFolderCreated })] }));
|
|
2161
|
+
}, onChanged: patchListedMessage, labels: labels, onLabelsChanged: patchListedMessage, onLabelCreated: (label) => (otherMailbox ? setOtherMailboxLabels : setMailboxLabels)((prev) => [...prev, label]) })) }), _jsx(MoveToFolderDialog, { open: moveDialog !== null, onClose: () => setMoveDialog(null), mailboxUid: activeMailboxUid, folders: currentFolders, currentFolderUid: folderUid, count: moveDialog === null ? 1 : "messages" in moveDialog ? moveDialog.messages.length : moveDialog.conversation.messageCount, onMove: swipeMove, onFolderCreated: onFolderCreated })] }));
|
|
2066
2162
|
}
|
|
2067
|
-
export default
|
|
2163
|
+
export default InboxPage;
|
|
2164
|
+
/** The tab's title: `Brand: Mail` (see `pageTitle()`). */
|
|
2165
|
+
export const title = pageTitle("Mail");
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { MailShellProps } from "../../shared/components/mail/layout/MailShell.js";
|
|
2
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Only reached on mobile (below the `md` breakpoint) — desktop's `apps/www/index.tsx` keeps its existing
|
|
5
|
+
* inline reading pane and never navigates here; see that file's `handleSelect`. A row of the conversation list
|
|
6
|
+
* comes with `?conversation=<id>` (`handleOpenConversation`), and then this page is the whole thread, opened at this
|
|
7
|
+
* message, as the desktop's reading pane is; without it (a flat list, a link to a message) it is that one message.
|
|
8
|
+
*/
|
|
9
|
+
declare function MessageDetailPage(props: MailShellProps & {
|
|
3
10
|
params: {
|
|
4
11
|
uid: string;
|
|
5
12
|
};
|
|
6
|
-
}
|
|
7
|
-
export default
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
export default MessageDetailPage;
|
|
15
|
+
/** The tab's title: `Brand: Mail` (see `pageTitle()`). */
|
|
16
|
+
export declare const title: (props: {
|
|
17
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
18
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useEffect, useState } from "react";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { getMessage } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
@@ -12,7 +12,7 @@ import { useMessageAttachments } from "@rapidmx/react-shared/mail/mailDetailHook
|
|
|
12
12
|
import { useMarkMessageRead } from "../../shared/mail/useMarkMessageRead.js";
|
|
13
13
|
import MailShell, { useMailShell } from "../../shared/components/mail/layout/MailShell.js";
|
|
14
14
|
import { LazyConversationThreadPane, LazyMessageDetailPane } from "../../shared/components/mail/LazyReadingPane.js";
|
|
15
|
-
import {
|
|
15
|
+
import { useSearchParams } from "@rapidrest/react/client";
|
|
16
16
|
import { ReadingPaneSkeleton } from "../../shared/components/mail/reading/MessageCard.js";
|
|
17
17
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
18
18
|
/**
|
|
@@ -30,7 +30,7 @@ function MessageDetailContent({ uid }) {
|
|
|
30
30
|
const [loading, setLoading] = useState(true);
|
|
31
31
|
const [error, setError] = useState(null);
|
|
32
32
|
const [labels, setLabels] = useState([]);
|
|
33
|
-
const conversationId =
|
|
33
|
+
const conversationId = useSearchParams()[0].get("conversation");
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
setLoading(true);
|
|
36
36
|
setError(null);
|
|
@@ -80,6 +80,8 @@ function MessageDetailContent({ uid }) {
|
|
|
80
80
|
if (conversationId) {
|
|
81
81
|
return (_jsxs("div", { className: "flex flex-col h-full min-h-0", children: [_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" }), _jsx(LazyConversationThreadPane, { conversation: { conversationId, subject: message.subject, messageCount: 1 }, selectedUid: message.uid, mailboxUid: message.mailboxUid, folders: folders, labels: labels, shortcuts: true, onMessagePatched: () => undefined, onMessageRemoved: () => undefined, onLabelCreated: (label) => setLabels((prev) => [...prev, label]), onFolderCreated: onFolderCreated })] }));
|
|
82
82
|
}
|
|
83
|
-
return (_jsx(LazyMessageDetailPane, { shortcuts: true, message: message, attachments: attachments, backHref: backHref, isSentItems: isSentItems, onRecalled: setMessage, isOutbox: isOutbox, onReceiptHandled: setMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: setMessage, onFolderCreated: onFolderCreated, onScheduledSendCanceled: setMessage, onArchived: setMessage, labels: labels, onLabelsChanged: setMessage, onLabelCreated: (label) => setLabels((prev) => [...prev, label]) }));
|
|
83
|
+
return (_jsx(LazyMessageDetailPane, { shortcuts: true, message: message, attachments: attachments, backHref: backHref, isSentItems: isSentItems, onRecalled: setMessage, isOutbox: isOutbox, onReceiptHandled: setMessage, draftsFolderUid: draftsFolderUid, folders: folders, onMoved: setMessage, onFolderCreated: onFolderCreated, onScheduledSendCanceled: setMessage, onArchived: setMessage, onChanged: setMessage, labels: labels, onLabelsChanged: setMessage, onLabelCreated: (label) => setLabels((prev) => [...prev, label]) }));
|
|
84
84
|
}
|
|
85
|
-
export default
|
|
85
|
+
export default MessageDetailPage;
|
|
86
|
+
/** The tab's title: `Brand: Mail` (see `pageTitle()`). */
|
|
87
|
+
export const title = pageTitle("Mail");
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type SettingsAppearancePageProps = Omit<SettingsShellProps, "active">;
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/** Settings > Appearance: the colour scheme, theme colours and background. Not mailbox-scoped - it is the signed-in user's own. */
|
|
5
|
+
declare function SettingsAppearancePage(props: SettingsAppearancePageProps): React.JSX.Element;
|
|
6
|
+
export default SettingsAppearancePage;
|
|
7
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
8
|
+
export declare const title: (props: {
|
|
9
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
10
|
+
}) => string;
|
|
@@ -3,11 +3,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import SettingsShell from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
8
8
|
import AppearanceForm from "../../../shared/appearance/AppearanceForm.js";
|
|
9
9
|
/** Settings > Appearance: the colour scheme, theme colours and background. Not mailbox-scoped - it is the signed-in user's own. */
|
|
10
10
|
function SettingsAppearancePage(props) {
|
|
11
11
|
return (_jsx(SettingsShell, { ...props, active: "appearance", children: _jsx(AppearanceForm, {}) }));
|
|
12
12
|
}
|
|
13
|
-
export default
|
|
13
|
+
export default SettingsAppearancePage;
|
|
14
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
15
|
+
export const title = pageTitle("Settings");
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
2
3
|
export type SettingsAutoReplyPageProps = Omit<SettingsShellProps, "active">;
|
|
3
|
-
declare
|
|
4
|
-
export default
|
|
4
|
+
declare function SettingsAutoReplyPage(props: SettingsAutoReplyPageProps): React.JSX.Element;
|
|
5
|
+
export default SettingsAutoReplyPage;
|
|
6
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
7
|
+
export declare const title: (props: {
|
|
8
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
9
|
+
}) => string;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { pageTitle } from "../../../shared/navigation/pageTitle.js";
|
|
7
7
|
import { useState } from "react";
|
|
8
8
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
9
9
|
import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
@@ -55,4 +55,6 @@ function AutoReplyContent() {
|
|
|
55
55
|
}
|
|
56
56
|
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Automatic Replies" }), _jsxs("p", { className: "text-sm text-text-muted mb-4", children: ["Send an automatic reply to anyone who emails ", mailbox.displayName, " while you\u2019re away. A calendar event with its own automatic reply enabled takes over for its own start/end window."] }), saved && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx("input", { type: "checkbox", checked: oofEnabled, onChange: (e) => setOofEnabled(e.target.checked) }), "Automatic replies are on"] }), oofEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Message" }), _jsx("textarea", { "aria-label": "Automatic reply message", className: INPUT_CLASS, rows: 5, value: oofMessage, onChange: (e) => setOofMessage(e.target.value) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Start (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply start", className: INPUT_CLASS, value: oofStartTime, onChange: (e) => setOofStartTime(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "End (optional)" }), _jsx("input", { type: "datetime-local", "aria-label": "Automatic reply end", className: INPUT_CLASS, value: oofEndTime, onChange: (e) => setOofEndTime(e.target.value) })] })] }), _jsx("p", { className: "text-xs text-text-muted -mt-2", children: "Leave both blank to stay on indefinitely until turned off." })] })), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
57
57
|
}
|
|
58
|
-
export default
|
|
58
|
+
export default SettingsAutoReplyPage;
|
|
59
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
60
|
+
export const title = pageTitle("Settings");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { SettingsShellProps } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
3
|
+
export type SettingsBlockedSendersPageProps = Omit<SettingsShellProps, "active">;
|
|
4
|
+
declare function SettingsBlockedSendersPage(props: SettingsBlockedSendersPageProps): React.JSX.Element;
|
|
5
|
+
export default SettingsBlockedSendersPage;
|
|
6
|
+
/** The tab's title: `Brand: Settings` (see `pageTitle()`). */
|
|
7
|
+
export declare const title: (props: {
|
|
8
|
+
branding?: Pick<import("@rapidmx/react-shared/branding/brandingApi.js").Branding, "title" | "companyName"> | null;
|
|
9
|
+
}) => string;
|