@rapidmx/web-client 0.3.0 → 0.4.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/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/index.tsx +100 -82
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
- package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +40 -4
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactForm.tsx +38 -4
- package/apps/shared/components/layout/AppShell.tsx +198 -169
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
- package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
- package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +179 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +116 -0
- package/apps/shared/search/localIndexSchema.ts +227 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexVFS.ts +235 -0
- package/apps/shared/search/localIndexWorker.ts +464 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +31 -21
- package/apps/www/contacts/index.tsx +33 -6
- package/apps/www/index.tsx +661 -106
- package/apps/www/messages/[uid].tsx +5 -1
- package/apps/www/settings/encryption/index.tsx +63 -1
- package/apps/www/settings/sharing/index.tsx +271 -0
- package/apps/www/tasks/index.tsx +53 -4
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/index.js +14 -1
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +16 -4
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
- package/dist/apps/shared/components/layout/AppShell.js +30 -7
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
- package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +153 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
- package/dist/apps/shared/search/localIndexSchema.js +179 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexVFS.js +230 -0
- package/dist/apps/shared/search/localIndexWorker.js +341 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +14 -12
- package/dist/apps/www/contacts/index.js +15 -6
- package/dist/apps/www/index.js +496 -94
- package/dist/apps/www/messages/[uid].js +5 -1
- package/dist/apps/www/settings/encryption/index.js +30 -2
- package/dist/apps/www/settings/sharing/index.js +128 -0
- package/dist/apps/www/tasks/index.js +27 -5
- package/package.json +3 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { deriveLocalIndexKey } from "./localIndexKey.js";
|
|
2
|
+
import { getLocalCoverage, initLocalIndex, searchLocal } from "./localIndexRpcClient.js";
|
|
3
|
+
/** Ensures this mailbox's local index connection is open before it's queried - idempotent
|
|
4
|
+
* (`localIndexWorker.ts`'s own `init()` no-ops for an already-open mailbox), so callers never need to
|
|
5
|
+
* coordinate with `LocalIndexLifecycle.tsx`'s own unlock-triggered `init()` call; whichever runs first
|
|
6
|
+
* wins, and the other is a cheap no-op. */
|
|
7
|
+
async function ensureInitialized(mailboxUid, unlocked) {
|
|
8
|
+
const indexKey = await deriveLocalIndexKey(unlocked.masterKey, mailboxUid);
|
|
9
|
+
await initLocalIndex({ mailboxUid, indexKey });
|
|
10
|
+
}
|
|
11
|
+
export async function searchLocalIndex(mailboxUid, parsed, unlocked, limit = 50, offset = 0) {
|
|
12
|
+
if (!unlocked) {
|
|
13
|
+
return { results: [], hasMore: false };
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
await ensureInitialized(mailboxUid, unlocked);
|
|
17
|
+
const [page, coverage] = await Promise.all([searchLocal(mailboxUid, parsed, limit, offset), getLocalCoverage(mailboxUid)]);
|
|
18
|
+
const results = page.hits.map((hit) => ({
|
|
19
|
+
entityType: "message",
|
|
20
|
+
entityUid: hit.entityUid,
|
|
21
|
+
// Negated: bm25()'s own convention is "more negative is a better match" (SQLite), the
|
|
22
|
+
// opposite of every other score this codebase merges (Postgres ts_rank, OpenSearch _score,
|
|
23
|
+
// and normalizeServerScores() itself all treat "higher is better"). Negating here, once,
|
|
24
|
+
// keeps that convention uniform for the caller (apps/www/index.tsx's merge layer) - it never
|
|
25
|
+
// needs to know this tier's underlying scoring function works backwards from the others.
|
|
26
|
+
score: -hit.score,
|
|
27
|
+
snippet: hit.snippet,
|
|
28
|
+
source: "local",
|
|
29
|
+
metadataOnly: false,
|
|
30
|
+
}));
|
|
31
|
+
return { results, coverage, hasMore: page.hasMore };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// See this function's own doc comment - a broken local index degrades to "nothing to contribute
|
|
35
|
+
// this time," never a rejected search.
|
|
36
|
+
return { results: [], hasMore: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -8,7 +8,6 @@ import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from "@dn
|
|
|
8
8
|
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
9
9
|
import { addDays, addMonths, addWeeks, eachDayOfInterval, endOfDay, endOfMonth, endOfWeek, format, startOfDay, startOfMonth, startOfWeek, } from "date-fns";
|
|
10
10
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
11
|
-
import { colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
|
|
12
11
|
import { listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
13
12
|
import { moveOccurrence, resizeOccurrenceEnd } from "@rapidmx/react-shared/calendar/calendarMutations.js";
|
|
14
13
|
import { resolveDragAction } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
|
|
@@ -30,10 +29,7 @@ export default function CalendarPage(props) {
|
|
|
30
29
|
const VIEW_TYPES = ["month", "week", "workWeek", "day", "split"];
|
|
31
30
|
const VIEW_LABELS = { month: "Month", week: "Week", workWeek: "Work Week", day: "Day", split: "Split" };
|
|
32
31
|
function CalendarContent() {
|
|
33
|
-
const { mailboxUid, folderUid, calendarFolders, mailboxes, reloadFolders } = useCalendarShell();
|
|
34
|
-
// `CalendarShell` only ever renders this component once `mailboxUid` is set, and always to a value
|
|
35
|
-
// drawn from `mailboxes` itself (see its own resolution logic) — the lookup below always succeeds.
|
|
36
|
-
const organizerAddress = mailboxes.find((mb) => mb.uid === mailboxUid).primarySmtpAddress;
|
|
32
|
+
const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } = useCalendarShell();
|
|
37
33
|
// `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
|
|
38
34
|
// scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
|
|
39
35
|
// immediately on a deliberate movement, no change from before) + `TouchSensor` (a `delay`+`tolerance`
|
|
@@ -65,7 +61,7 @@ function CalendarContent() {
|
|
|
65
61
|
}
|
|
66
62
|
}, []);
|
|
67
63
|
const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
|
|
68
|
-
const folderColors = useMemo(() => Object.fromEntries(calendarFolders.map((f) => [f.uid,
|
|
64
|
+
const folderColors = useMemo(() => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])), [calendarFolders, colorFor]);
|
|
69
65
|
function toggleCalendar(toggledUid) {
|
|
70
66
|
setCheckedFolderUidsState((prev) => {
|
|
71
67
|
const next = new Set(prev ?? calendarFolders.map((f) => f.uid));
|
|
@@ -78,9 +74,8 @@ function CalendarContent() {
|
|
|
78
74
|
return next;
|
|
79
75
|
});
|
|
80
76
|
}
|
|
81
|
-
async function handleAddCalendar(name, color) {
|
|
82
|
-
|
|
83
|
-
const created = await createFolder({ mailboxUid: mailboxUid, name, type: "calendar", color });
|
|
77
|
+
async function handleAddCalendar(targetMailboxUid, name, color) {
|
|
78
|
+
const created = await createFolder({ mailboxUid: targetMailboxUid, name, type: "calendar", color });
|
|
84
79
|
setCheckedFolderUidsState((prev) => new Set([...(prev ?? calendarFolders.map((f) => f.uid)), created.uid]));
|
|
85
80
|
reloadFolders();
|
|
86
81
|
}
|
|
@@ -152,7 +147,7 @@ function CalendarContent() {
|
|
|
152
147
|
const occurrences = useMemo(() => expandAllOccurrences(events, rangeStart, rangeEnd), [events, rangeStart, rangeEnd]);
|
|
153
148
|
const splitColumns = useMemo(() => calendarFolders
|
|
154
149
|
.filter((f) => checkedFolderUids.has(f.uid))
|
|
155
|
-
.map((f) => ({ folderUid: f.uid, name: f.name, color:
|
|
150
|
+
.map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })), [calendarFolders, checkedFolderUidsKey, colorFor]);
|
|
156
151
|
function shiftView(direction) {
|
|
157
152
|
setViewDate((d) => view === "month"
|
|
158
153
|
? addMonths(d, direction)
|
|
@@ -217,12 +212,19 @@ function CalendarContent() {
|
|
|
217
212
|
: view === "week" || view === "workWeek"
|
|
218
213
|
? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
|
|
219
214
|
: format(viewDate, "EEEE, MMMM d, yyyy");
|
|
220
|
-
const
|
|
215
|
+
const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
|
|
216
|
+
const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
|
|
217
|
+
const modalMailbox = mailboxes.find((mb) => mb.uid === modalMailboxUid);
|
|
218
|
+
// Mailboxes a new event can be created in - only those with at least one calendar.
|
|
219
|
+
const mailboxOptions = useMemo(() => mailboxCalendars
|
|
220
|
+
.filter((mc) => mc.calendarFolders.length > 0)
|
|
221
|
+
.map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })), [mailboxCalendars]);
|
|
222
|
+
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 })] }));
|
|
221
223
|
return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx("div", { className: "hidden md: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", children: _jsx("div", { className: "flex flex-col", children: sidebarContent }) }), _jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [_jsxs("div", { className: "flex items-center gap-3 p-3 border-b border-border shrink-0", children: [_jsx("button", { type: "button", className: "md: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" }) }), _jsx(Button, { type: "button", onClick: () => openNewEvent(), className: "!w-auto shrink-0", children: "+ New event" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", onClick: () => shiftView(-1), "aria-label": "Previous", className: "w-7 h-7 text-sm rounded-sm hover:bg-surface-alt", children: "\u2039" }), _jsx("button", { type: "button", onClick: goToday, 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", 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 gap-1 overflow-x-auto", children: VIEW_TYPES.map((v) => (_jsx("button", { type: "button", onClick: () => setView(v), className: [
|
|
222
224
|
"px-3 h-7 text-sm rounded-sm whitespace-nowrap",
|
|
223
225
|
view === v ? "bg-primary text-white" : "hover:bg-surface-alt",
|
|
224
226
|
(v === "workWeek" || v === "split") && "hidden md:inline-block",
|
|
225
227
|
]
|
|
226
228
|
.filter(Boolean)
|
|
227
|
-
.join(" "), children: VIEW_LABELS[v] }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), actionError && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: actionError }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), modal && (_jsx(EventModal, { open: true, onClose: closeModal, mailboxUid:
|
|
229
|
+
.join(" "), children: VIEW_LABELS[v] }, v))) })] }), error && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: error }) })), actionError && (_jsx("div", { className: "p-3", children: _jsx(Alert, { children: actionError }) })), loading ? (_jsx("p", { className: "p-4 text-sm text-text-muted", children: "Loading\u2026" })) : (_jsx(DndContext, { sensors: sensors, onDragEnd: handleDragEnd, children: view === "month" ? (_jsx(MonthView, { viewDate: viewDate, occurrences: occurrences, folderColors: folderColors, onSelectDay: handleSelectDay, onSelectEvent: openEvent })) : view === "split" ? (_jsx(SplitDayView, { day: viewDate, columns: splitColumns, occurrences: occurrences, onSelectEvent: openEvent, onSelectSlot: (start, end, targetFolderUid) => openNewEvent(start, end, targetFolderUid) })) : (_jsx(TimeGridView, { days: days, occurrences: occurrences, folderColors: folderColors, onSelectEvent: openEvent, onSelectSlot: openNewEvent })) })), 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, organizerAddress: modalMailbox.primarySmtpAddress, occurrence: modal.occurrence, initialStart: modal.initialStart, initialEnd: modal.initialEnd, onSaved: handleSaved, onDeleted: handleDeleted }))] })] }));
|
|
228
230
|
}
|
|
@@ -33,7 +33,7 @@ function downloadTextFile(filename, content) {
|
|
|
33
33
|
URL.revokeObjectURL(url);
|
|
34
34
|
}
|
|
35
35
|
function ContactsContent() {
|
|
36
|
-
const { folderUid, mailboxUid } = useContactsShell();
|
|
36
|
+
const { folderUid, mailboxUid, mailboxes } = useContactsShell();
|
|
37
37
|
const { openCompose } = useCompose();
|
|
38
38
|
const isMobile = useIsMobile();
|
|
39
39
|
const [contacts, setContacts] = useState([]);
|
|
@@ -49,6 +49,8 @@ function ContactsContent() {
|
|
|
49
49
|
const [checkedUids, setCheckedUids] = useState(new Set());
|
|
50
50
|
const [selectedUid, setSelectedUid] = useState(null);
|
|
51
51
|
const [mode, setMode] = useState("view");
|
|
52
|
+
// Set after a contact is created in a different mailbox than the one this list shows.
|
|
53
|
+
const [savedElsewhere, setSavedElsewhere] = useState(null);
|
|
52
54
|
/** Returns a promise so bulk actions (below) can wait for the refreshed list before re-asserting their
|
|
53
55
|
* own error message — this always clears `error` first (a legitimate reset for a fresh fetch attempt),
|
|
54
56
|
* which would otherwise silently wipe out a bulk action's own just-set failure message before the user
|
|
@@ -156,8 +158,15 @@ function ContactsContent() {
|
|
|
156
158
|
setMode("new");
|
|
157
159
|
}
|
|
158
160
|
function handleSaved(contact) {
|
|
159
|
-
setSelectedUid(contact.uid);
|
|
160
161
|
setMode("view");
|
|
162
|
+
if (contact.mailboxUid !== mailboxUid) {
|
|
163
|
+
// Created in another mailbox (via the form's Mailbox selector) - it won't be in this list.
|
|
164
|
+
setSelectedUid(null);
|
|
165
|
+
setSavedElsewhere({ mailboxUid: contact.mailboxUid, displayName: contact.displayName });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setSavedElsewhere(null);
|
|
169
|
+
setSelectedUid(contact.uid);
|
|
161
170
|
void reload();
|
|
162
171
|
}
|
|
163
172
|
function handleCancel() {
|
|
@@ -198,9 +207,9 @@ function ContactsContent() {
|
|
|
198
207
|
}
|
|
199
208
|
function handleEmail() {
|
|
200
209
|
const addresses = checkedContacts.map((c) => c.emails[0]?.address).filter((a) => Boolean(a));
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
openCompose({
|
|
210
|
+
// No mailboxUid: a fresh message defaults to the caller's own mailbox, not whichever mailbox's
|
|
211
|
+
// contacts happen to be open - the compose window's From field can still change it.
|
|
212
|
+
openCompose({ to: addresses.join(", ") });
|
|
204
213
|
}
|
|
205
214
|
async function handleToggleFavorite() {
|
|
206
215
|
const allFavorited = checkedContacts.every((c) => c.favorite);
|
|
@@ -258,5 +267,5 @@ function ContactsContent() {
|
|
|
258
267
|
return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(ContactsSidebar, { mailboxUid: mailboxUid, contacts: contacts, active: view, onSelect: handleSelectView }), _jsxs("div", { className: [
|
|
259
268
|
"w-full md:w-96 shrink-0 md:border-r border-border md:flex flex-col min-h-0",
|
|
260
269
|
mode === "new" ? "hidden md:flex" : "flex",
|
|
261
|
-
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: handleBulkDelete, onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { 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 }) })), 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 overflow-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "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: "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 items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "px-3 py-2 text-text-muted truncate", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }),
|
|
270
|
+
].join(" "), children: [_jsx(ContactsToolbar, { selectedCount: checkedContacts.length, allSelectedFavorited: checkedContacts.length > 0 && checkedContacts.every((c) => c.favorite), onNewContact: handleNew, onEdit: handleToolbarEdit, onDelete: handleBulkDelete, onEmail: handleEmail, onToggleFavorite: handleToggleFavorite, onAddCategory: handleAddCategory, onExportVCard: handleExportVCard, onImportFile: handleImportFile }), _jsx("div", { className: "p-3 border-b border-border", children: _jsx("input", { 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 }) })), 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 overflow-auto", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [!isDeletedView && (_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all contacts", checked: allChecked, onChange: toggleCheckAll }) })), _jsx("th", { className: "px-3 py-2", children: _jsxs("button", { type: "button", onClick: () => handleSort("name"), className: "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: "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 items-center gap-2 text-left", children: [_jsx(ContactAvatar, { displayName: contact.displayName, size: 28 }), _jsxs("span", { className: "truncate font-medium", children: [contact.displayName, contact.favorite && _jsx("span", { "aria-label": "Favorite", children: " \u2605" })] })] }) }), _jsx("td", { className: "px-3 py-2 text-text-muted truncate", children: primaryInfo(contact) })] }, contact.uid))) })] }) }))] }), _jsxs("div", { className: ["flex-1 min-w-0 overflow-y-auto p-6 md:block", mode === "new" ? "block" : "hidden"].join(" "), children: [savedElsewhere && mode !== "new" && (_jsxs("p", { role: "status", className: "mb-4 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: mailboxes, onSaved: handleSaved, onCancel: handleCancel })) : mode === "edit" && selected ? (_jsx(ContactForm, { contact: selected, onSaved: handleSaved, onCancel: handleCancel })) : selected ? (_jsx(ContactDetailPane, { contact: selected, onEdit: () => setMode("edit"), onDelete: () => handleDelete(selected) })) : (_jsx("p", { className: "text-sm text-text-muted", children: "Select a contact, or create a new one." }))] })] }));
|
|
262
271
|
}
|