@rapidmx/web-client 0.28.0 → 0.30.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 +3 -0
- package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +22 -10
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +60 -0
- package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +108 -0
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +25 -1
- package/apps/shared/components/calendar/CalendarSearchBox.tsx +83 -0
- package/apps/shared/components/calendar/EventMatchNav.tsx +72 -0
- package/apps/shared/components/calendar/EventModal.tsx +33 -12
- package/apps/shared/components/calendar/ListView.tsx +207 -0
- package/apps/shared/components/calendar/MonthView.tsx +14 -2
- package/apps/shared/components/calendar/SlideIn.tsx +47 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +5 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +23 -12
- package/apps/shared/components/calendar/activeOccurrence.ts +28 -0
- package/apps/shared/components/calendar/allDay.ts +16 -1
- package/apps/shared/components/calendar/calendarView.ts +41 -0
- package/apps/shared/components/layout/AppShell.tsx +13 -6
- package/apps/shared/components/mail/ConversationList.tsx +38 -5
- package/apps/shared/components/mail/ConversationThreadPane.tsx +15 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +117 -55
- package/apps/shared/components/mail/MessageDetailPane.tsx +3 -0
- package/apps/shared/components/mail/SwipeRow.tsx +26 -4
- package/apps/shared/components/mail/compose/ComposeContext.tsx +193 -35
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +62 -11
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +21 -10
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +2 -1
- package/apps/shared/components/mail/compose/quotedBody.ts +45 -2
- package/apps/shared/components/mail/labelMenu.tsx +4 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +9 -2
- package/apps/shared/components/mail/reading/MessageBody.tsx +1 -1
- package/apps/shared/components/mail/reading/bodyContent.ts +15 -13
- package/apps/shared/components/mail/reading/bodyHtml.ts +12 -7
- package/apps/shared/gestures/useEnterSlide.ts +1 -1
- package/apps/shared/gestures/useLongPress.ts +138 -0
- package/apps/shared/keyboard/keymap.ts +2 -0
- package/apps/shared/mail/rangeSelection.ts +45 -0
- package/apps/shared/mail/rowExit.ts +39 -0
- package/apps/www/calendar/index.tsx +251 -53
- package/apps/www/index.tsx +570 -358
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +3 -3
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +15 -9
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +15 -0
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +16 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +20 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +31 -0
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +20 -1
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +7 -0
- package/dist/apps/shared/components/calendar/CalendarSearchBox.d.ts +22 -0
- package/dist/apps/shared/components/calendar/CalendarSearchBox.js +24 -0
- package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +17 -0
- package/dist/apps/shared/components/calendar/EventMatchNav.js +37 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +17 -1
- package/dist/apps/shared/components/calendar/EventModal.js +13 -11
- package/dist/apps/shared/components/calendar/ListView.d.ts +30 -0
- package/dist/apps/shared/components/calendar/ListView.js +111 -0
- package/dist/apps/shared/components/calendar/MonthView.js +12 -3
- package/dist/apps/shared/components/calendar/SlideIn.d.ts +16 -0
- package/dist/apps/shared/components/calendar/SlideIn.js +32 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +5 -1
- package/dist/apps/shared/components/calendar/TimeGridView.js +11 -3
- package/dist/apps/shared/components/calendar/activeOccurrence.d.ts +15 -0
- package/dist/apps/shared/components/calendar/activeOccurrence.js +21 -0
- package/dist/apps/shared/components/calendar/allDay.d.ts +4 -0
- package/dist/apps/shared/components/calendar/allDay.js +14 -1
- package/dist/apps/shared/components/calendar/calendarView.d.ts +16 -0
- package/dist/apps/shared/components/calendar/calendarView.js +36 -0
- package/dist/apps/shared/components/layout/AppShell.js +8 -4
- package/dist/apps/shared/components/mail/ConversationList.d.ts +16 -3
- package/dist/apps/shared/components/mail/ConversationList.js +10 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +9 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
- package/dist/apps/shared/components/mail/MessageDetailPane.js +3 -0
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +5 -1
- package/dist/apps/shared/components/mail/SwipeRow.js +17 -3
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +31 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +123 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +6 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +37 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +5 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +10 -6
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +2 -1
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +3 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +43 -1
- package/dist/apps/shared/components/mail/labelMenu.d.ts +3 -1
- package/dist/apps/shared/components/mail/labelMenu.js +2 -2
- package/dist/apps/shared/components/mail/layout/MailShell.js +7 -1
- package/dist/apps/shared/components/mail/reading/MessageBody.js +1 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +5 -1
- package/dist/apps/shared/components/mail/reading/bodyContent.js +11 -11
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +3 -2
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +8 -4
- package/dist/apps/shared/gestures/useEnterSlide.d.ts +2 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +1 -1
- package/dist/apps/shared/gestures/useLongPress.d.ts +41 -0
- package/dist/apps/shared/gestures/useLongPress.js +112 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +12 -0
- package/dist/apps/shared/keyboard/keymap.js +2 -0
- package/dist/apps/shared/mail/rangeSelection.d.ts +20 -0
- package/dist/apps/shared/mail/rangeSelection.js +34 -0
- package/dist/apps/shared/mail/rowExit.d.ts +12 -0
- package/dist/apps/shared/mail/rowExit.js +33 -0
- package/dist/apps/www/calendar/index.js +177 -18
- package/dist/apps/www/index.js +257 -73
- package/dist/lib/calendar/eventSearch.d.ts +42 -0
- package/dist/lib/calendar/eventSearch.js +152 -0
- package/dist/lib/components/navigation/BottomTabBar.d.ts +15 -3
- package/dist/lib/components/navigation/BottomTabBar.js +52 -15
- package/dist/lib/components/navigation/NavBadge.d.ts +13 -0
- package/dist/lib/components/navigation/NavBadge.js +18 -0
- package/dist/lib/mail/compose/quotedImages.d.ts +21 -0
- package/dist/lib/mail/compose/quotedImages.js +41 -0
- package/dist/lib/mail/inlineImages.d.ts +38 -0
- package/dist/lib/mail/inlineImages.js +90 -0
- package/dist/lib/mail/mailApi.d.ts +8 -1
- package/dist/lib/mail/mailApi.js +57 -5
- package/lib/calendar/eventSearch.ts +172 -0
- package/lib/components/navigation/BottomTabBar.tsx +107 -22
- package/lib/components/navigation/NavBadge.tsx +33 -0
- package/lib/mail/compose/quotedImages.ts +49 -0
- package/lib/mail/inlineImages.ts +104 -0
- package/lib/mail/mailApi.ts +64 -5
- package/package.json +1 -1
- package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +0 -36
- package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +0 -10
- package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +0 -11
|
@@ -15,6 +15,7 @@ import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
|
15
15
|
import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
|
|
16
16
|
import SwipeRow from "./SwipeRow.js";
|
|
17
17
|
import InviteRowChip from "./invite/InviteRowChip.js";
|
|
18
|
+
import { ClickModifiers, isSelectionClick } from "../../mail/rangeSelection.js";
|
|
18
19
|
|
|
19
20
|
/** A conversation row of the list. `mailboxUid` is set only by a view that merges several mailboxes ("All Mailboxes"), whose rows are
|
|
20
21
|
* each from a different one; a list of one mailbox's own leaves it out and every row is that mailbox's. */
|
|
@@ -50,15 +51,27 @@ export interface ConversationListProps {
|
|
|
50
51
|
/** The `conversationRowKey()`s currently ticked. */
|
|
51
52
|
selectedConversationIds?: Set<string>;
|
|
52
53
|
onToggleSelected?: (conversation: ListedConversation) => void;
|
|
54
|
+
/**
|
|
55
|
+
* A Shift, Ctrl or Cmd click on a parent row (or on its checkbox): the caller extends or toggles the selection instead of this list opening or
|
|
56
|
+
* ticking the row itself (`onOpenMessage` / `onToggleSelected` are not called for it). Absent, those clicks behave as plain ones.
|
|
57
|
+
*/
|
|
58
|
+
onSelectWithModifier?: (conversation: ListedConversation, modifiers: ClickModifiers) => void;
|
|
59
|
+
/** The `conversationRowKey()`s of rows on their way out of the list (moved, deleted): drawn non-interactive and hidden from assistive technology while the page collapses them. */
|
|
60
|
+
exitingKeys?: Set<string>;
|
|
53
61
|
/**
|
|
54
62
|
* What swiping a parent row does on a phone: right to left archives the conversation (resolving whether it went through), left
|
|
55
63
|
* to right asks where to move it. Absent, or `enabled: false` (a desktop), rows don't follow a finger.
|
|
56
64
|
*/
|
|
57
65
|
swipe?: {
|
|
58
66
|
enabled: boolean;
|
|
59
|
-
onArchive: (conversation:
|
|
60
|
-
onMove: (conversation:
|
|
67
|
+
onArchive: (conversation: ListedConversation) => Promise<boolean>;
|
|
68
|
+
onMove: (conversation: ListedConversation) => void;
|
|
61
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* A finger held on a parent row on a phone: the caller starts select mode with that conversation ticked. Absent, rows have no long press; it is not
|
|
72
|
+
* heard in select mode either, where a tap ticks a row already.
|
|
73
|
+
*/
|
|
74
|
+
onLongPress?: (conversation: ListedConversation) => void;
|
|
62
75
|
/**
|
|
63
76
|
* Whether a conversation's own messages read newest first, matching the order sense the rows themselves
|
|
64
77
|
* are in ("Newest on top"). `listConversationMessages()` always answers oldest first - the order a
|
|
@@ -95,7 +108,10 @@ export default function ConversationList({
|
|
|
95
108
|
selectMode,
|
|
96
109
|
selectedConversationIds,
|
|
97
110
|
onToggleSelected,
|
|
111
|
+
onSelectWithModifier,
|
|
112
|
+
exitingKeys,
|
|
98
113
|
swipe,
|
|
114
|
+
onLongPress,
|
|
99
115
|
newestFirst,
|
|
100
116
|
}: ConversationListProps) {
|
|
101
117
|
const client = useApiClient();
|
|
@@ -176,13 +192,21 @@ export default function ConversationList({
|
|
|
176
192
|
const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
|
|
177
193
|
const panelId = `conversation-messages-${conversation.mailboxUid ? `${conversation.mailboxUid}-` : ""}${conversation.conversationId}`;
|
|
178
194
|
const ticked = selectedConversationIds?.has(id) ?? false;
|
|
195
|
+
const exiting = exitingKeys?.has(id) ?? false;
|
|
179
196
|
return (
|
|
180
|
-
<li
|
|
197
|
+
<li
|
|
198
|
+
key={id}
|
|
199
|
+
data-exiting={exiting ? "true" : undefined}
|
|
200
|
+
aria-hidden={exiting ? "true" : undefined}
|
|
201
|
+
inert={exiting}
|
|
202
|
+
className={exiting ? "pointer-events-none" : undefined}
|
|
203
|
+
>
|
|
181
204
|
<SwipeRow
|
|
182
205
|
as="div"
|
|
183
206
|
enabled={!!swipe?.enabled && !selectMode}
|
|
184
207
|
onArchive={() => swipe!.onArchive(conversation)}
|
|
185
208
|
onMove={() => swipe!.onMove(conversation)}
|
|
209
|
+
onLongPress={onLongPress && !selectMode ? () => onLongPress(conversation) : undefined}
|
|
186
210
|
data-message-uid={conversation.latestMessageUid}
|
|
187
211
|
data-unread={unread ? "true" : undefined}
|
|
188
212
|
className={rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch")}
|
|
@@ -193,7 +217,12 @@ export default function ConversationList({
|
|
|
193
217
|
<input
|
|
194
218
|
type="checkbox"
|
|
195
219
|
checked={ticked}
|
|
196
|
-
onChange={() =>
|
|
220
|
+
onChange={(event) =>
|
|
221
|
+
// The change of a checkbox is caused by a click (or the Space key, which makes one): its modifiers are that event's.
|
|
222
|
+
onSelectWithModifier && isSelectionClick(event.nativeEvent as MouseEvent)
|
|
223
|
+
? onSelectWithModifier(conversation, event.nativeEvent as MouseEvent)
|
|
224
|
+
: onToggleSelected?.(conversation)
|
|
225
|
+
}
|
|
197
226
|
aria-label={`Select conversation: ${conversation.subject || "(no subject)"}`}
|
|
198
227
|
className="w-4 h-4 accent-primary"
|
|
199
228
|
/>
|
|
@@ -218,7 +247,11 @@ export default function ConversationList({
|
|
|
218
247
|
<button
|
|
219
248
|
type="button"
|
|
220
249
|
data-row-open
|
|
221
|
-
onClick={() =>
|
|
250
|
+
onClick={(event) =>
|
|
251
|
+
onSelectWithModifier && isSelectionClick(event)
|
|
252
|
+
? onSelectWithModifier(conversation, event)
|
|
253
|
+
: onOpenMessage(conversation, conversation.latestMessageUid)
|
|
254
|
+
}
|
|
222
255
|
className={["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" ")}
|
|
223
256
|
>
|
|
224
257
|
<UnreadLabel unread={unread} />
|
|
@@ -15,6 +15,7 @@ import { ENCRYPTED_SUBJECT_PLACEHOLDER, EncryptedPreview, displaySubject } from
|
|
|
15
15
|
import { useDecryptedMessages } from "../../mail/decryptedMessages.js";
|
|
16
16
|
import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
|
|
17
17
|
import PendingMessageCard from "./reading/PendingMessageCard.js";
|
|
18
|
+
import { InlineComposeSlot, useInlineCompose } from "./compose/ComposeContext.js";
|
|
18
19
|
import { useMailShell } from "./layout/MailShell.js";
|
|
19
20
|
import { setReadState } from "../../mail/messageReadState.js";
|
|
20
21
|
import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
|
|
@@ -202,6 +203,9 @@ export default function ConversationThreadPane({
|
|
|
202
203
|
const [error, setError] = useState<string | null>(null);
|
|
203
204
|
/** The message to scroll to and focus once it has rendered, or `null` once that has happened. */
|
|
204
205
|
const [pendingFocusUid, setPendingFocusUid] = useState<string | null>(null);
|
|
206
|
+
// The Reply / Reply all / Forward compose windows of this thread's messages, drawn as cards at the top of the list; the thread's messages are
|
|
207
|
+
// what makes the compose provider open a reply here rather than floating at the bottom right.
|
|
208
|
+
const inlineSessions = useInlineCompose(messages.map((message) => message.uid));
|
|
205
209
|
|
|
206
210
|
// Bumped on every conversation switch - an in-flight load/attachments/mark-read response carrying an
|
|
207
211
|
// older generation belongs to a superseded conversation and is dropped rather than applied.
|
|
@@ -538,6 +542,17 @@ export default function ConversationThreadPane({
|
|
|
538
542
|
</div>
|
|
539
543
|
) : (
|
|
540
544
|
<ul className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
|
|
545
|
+
{inlineSessions.map((session) => (
|
|
546
|
+
<InlineComposeSlot
|
|
547
|
+
key={session.id}
|
|
548
|
+
id={session.id}
|
|
549
|
+
onPlaced={(slot) => {
|
|
550
|
+
// Brought into view like a message that has just been sent: the same scroll, inside the list.
|
|
551
|
+
rowRefs.current[session.id] = slot;
|
|
552
|
+
setPendingFocusUid(session.id);
|
|
553
|
+
}}
|
|
554
|
+
/>
|
|
555
|
+
))}
|
|
541
556
|
{pendingCards.map((reply) => (
|
|
542
557
|
<li
|
|
543
558
|
key={reply.uid}
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
HiOutlineArchiveBox,
|
|
8
|
+
HiOutlineCheckCircle,
|
|
9
|
+
HiOutlineEnvelope,
|
|
10
|
+
HiOutlineEnvelopeOpen,
|
|
11
|
+
HiOutlineFlag,
|
|
12
|
+
HiOutlineFolderArrowDown,
|
|
13
|
+
HiOutlineMinusCircle,
|
|
14
|
+
HiOutlineNoSymbol,
|
|
15
|
+
HiOutlineTrash,
|
|
16
|
+
HiOutlineXMark,
|
|
17
|
+
} from "react-icons/hi2";
|
|
6
18
|
import { Folder, Message } from "../../../../lib/mail/mailApi.js";
|
|
7
19
|
import { Label } from "../../../../lib/mail/labelsApi.js";
|
|
8
20
|
import LabelMenuButton from "./labelMenu.js";
|
|
@@ -65,8 +77,43 @@ export interface MailSelectionBarProps {
|
|
|
65
77
|
labelsDisabledReason?: string;
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
/** The look of the bar's icon buttons - the reading pane's own command row (`ICON_BUTTON_CLASS` in `MessageDetailPane`), without its border, which nine of them in a row don't need. */
|
|
81
|
+
function iconClassName(destructive = false): string {
|
|
82
|
+
return `inline-flex items-center justify-center p-1.5 rounded-md text-sm ${destructive ? "text-danger" : "text-text"} hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** One of the bar's actions as an icon button: the action's name is its `aria-label` (so it has the accessible name its text button had) and its tooltip,
|
|
86
|
+
* unless there is a `reason` it is unavailable, or a keyboard shortcut to name (`title` and `aria-keyshortcuts` from `hint()`). */
|
|
87
|
+
function BarAction({
|
|
88
|
+
icon,
|
|
89
|
+
label,
|
|
90
|
+
onClick,
|
|
91
|
+
disabled,
|
|
92
|
+
reason,
|
|
93
|
+
destructive,
|
|
94
|
+
hint,
|
|
95
|
+
}: {
|
|
96
|
+
icon: React.ReactNode;
|
|
97
|
+
label: string;
|
|
98
|
+
onClick: () => void;
|
|
99
|
+
disabled?: boolean;
|
|
100
|
+
reason?: string;
|
|
101
|
+
destructive?: boolean;
|
|
102
|
+
hint?: { title?: string; "aria-keyshortcuts"?: string };
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-label={label}
|
|
108
|
+
onClick={onClick}
|
|
109
|
+
disabled={disabled}
|
|
110
|
+
className={iconClassName(destructive)}
|
|
111
|
+
{...hint}
|
|
112
|
+
title={reason ?? hint?.title ?? label}
|
|
113
|
+
>
|
|
114
|
+
{icon}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
70
117
|
}
|
|
71
118
|
|
|
72
119
|
/**
|
|
@@ -148,47 +195,67 @@ export default function MailSelectionBar({
|
|
|
148
195
|
onMove={onMoveTo}
|
|
149
196
|
onFolderCreated={onFolderCreated}
|
|
150
197
|
/>
|
|
151
|
-
<div className="flex items-center gap-
|
|
152
|
-
<span aria-live="polite" className="text-sm font-semibold text-text">
|
|
198
|
+
<div className="flex items-center gap-1 px-3 py-1.5 bg-surface-alt">
|
|
199
|
+
<span aria-live="polite" className="text-sm font-semibold text-text mr-auto">
|
|
153
200
|
{selectedCount}
|
|
154
201
|
{totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : ""} selected
|
|
155
202
|
</span>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
</button>
|
|
203
|
+
<BarAction
|
|
204
|
+
icon={<HiOutlineCheckCircle size={16} aria-hidden="true" />}
|
|
205
|
+
label="Select all"
|
|
206
|
+
onClick={onSelectAll}
|
|
207
|
+
disabled={allSelected || listedCount === 0}
|
|
208
|
+
/>
|
|
209
|
+
<BarAction icon={<HiOutlineMinusCircle size={16} aria-hidden="true" />} label="Clear" onClick={onClearSelection} disabled={selectedCount === 0} />
|
|
210
|
+
<BarAction icon={<HiOutlineXMark size={16} aria-hidden="true" />} label="Cancel" onClick={onCancel} />
|
|
165
211
|
</div>
|
|
212
|
+
{/* Icon-only, so all nine fit one row of the 24rem list column; wraps if the column is ever narrower. */}
|
|
166
213
|
<div className="flex flex-wrap items-center gap-0.5 px-1.5 py-1">
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
<BarAction
|
|
215
|
+
icon={<HiOutlineEnvelopeOpen size={16} aria-hidden="true" />}
|
|
216
|
+
label="Mark read"
|
|
217
|
+
onClick={() => onSetRead(true)}
|
|
218
|
+
disabled={none || busy}
|
|
219
|
+
hint={hint("Mark read", SHORTCUTS.mail.markRead)}
|
|
220
|
+
/>
|
|
221
|
+
<BarAction
|
|
222
|
+
icon={<HiOutlineEnvelope size={16} aria-hidden="true" />}
|
|
223
|
+
label="Mark unread"
|
|
224
|
+
onClick={() => onSetRead(false)}
|
|
225
|
+
disabled={none || busy}
|
|
226
|
+
hint={hint("Mark unread", SHORTCUTS.mail.markUnread)}
|
|
227
|
+
/>
|
|
228
|
+
<BarAction
|
|
229
|
+
icon={<HiOutlineFlag size={16} aria-hidden="true" />}
|
|
230
|
+
label="Flag"
|
|
231
|
+
onClick={() => onSetFlagged(true)}
|
|
232
|
+
disabled={none || busy}
|
|
233
|
+
hint={hint("Flag", SHORTCUTS.mail.flag)}
|
|
234
|
+
/>
|
|
235
|
+
<BarAction
|
|
236
|
+
icon={
|
|
237
|
+
// `hi2` has no flag-with-a-line-through glyph: the flag, struck through.
|
|
238
|
+
<span className="relative inline-flex" aria-hidden="true">
|
|
239
|
+
<HiOutlineFlag size={16} />
|
|
240
|
+
<span className="absolute left-1/2 top-[-1px] h-[18px] w-[1.5px] -translate-x-1/2 rotate-45 rounded bg-current" />
|
|
241
|
+
</span>
|
|
242
|
+
}
|
|
243
|
+
label="Unflag"
|
|
244
|
+
onClick={() => onSetFlagged(false)}
|
|
245
|
+
disabled={none || busy}
|
|
246
|
+
/>
|
|
247
|
+
<BarAction
|
|
248
|
+
icon={<HiOutlineArchiveBox size={16} aria-hidden="true" />}
|
|
249
|
+
label="Archive"
|
|
181
250
|
onClick={onArchive}
|
|
182
251
|
disabled={none || busy || !!archiveReason}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
>
|
|
186
|
-
Archive
|
|
187
|
-
</button>
|
|
252
|
+
reason={archiveReason}
|
|
253
|
+
/>
|
|
188
254
|
<LabelMenuButton
|
|
189
255
|
aria-label="Apply label"
|
|
190
256
|
label="Apply label"
|
|
191
|
-
|
|
257
|
+
iconOnly
|
|
258
|
+
className={iconClassName()}
|
|
192
259
|
labels={labels}
|
|
193
260
|
mailboxUid={mailboxUid}
|
|
194
261
|
onLabelCreated={onLabelCreated}
|
|
@@ -197,7 +264,7 @@ export default function MailSelectionBar({
|
|
|
197
264
|
onCommit={onApplyLabels}
|
|
198
265
|
busy={busy}
|
|
199
266
|
disabled={none || busy || !!labelsDisabledReason}
|
|
200
|
-
title={labelsDisabledReason}
|
|
267
|
+
title={labelsDisabledReason ?? "Apply label"}
|
|
201
268
|
note={
|
|
202
269
|
selected.length === 1
|
|
203
270
|
? "Ticked labels are applied, unticked ones removed."
|
|
@@ -207,33 +274,28 @@ export default function MailSelectionBar({
|
|
|
207
274
|
commit={{ label: "Apply" }}
|
|
208
275
|
clear={{ label: "Remove all labels" }}
|
|
209
276
|
/>
|
|
210
|
-
<
|
|
211
|
-
|
|
277
|
+
<BarAction
|
|
278
|
+
icon={<HiOutlineFolderArrowDown size={16} aria-hidden="true" />}
|
|
279
|
+
label="Move to"
|
|
212
280
|
onClick={() => setMovePrompt(true)}
|
|
213
281
|
disabled={none || busy || !!moveDisabledReason}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<button
|
|
220
|
-
type="button"
|
|
282
|
+
reason={moveDisabledReason}
|
|
283
|
+
/>
|
|
284
|
+
<BarAction
|
|
285
|
+
icon={<HiOutlineNoSymbol size={16} aria-hidden="true" />}
|
|
286
|
+
label="Report junk"
|
|
221
287
|
onClick={onReportJunk}
|
|
222
288
|
disabled={none || busy || !!junkReason}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<button
|
|
229
|
-
type="button"
|
|
289
|
+
reason={junkReason}
|
|
290
|
+
/>
|
|
291
|
+
<BarAction
|
|
292
|
+
icon={<HiOutlineTrash size={16} aria-hidden="true" />}
|
|
293
|
+
label={deleteLabel}
|
|
230
294
|
onClick={onDelete}
|
|
231
295
|
disabled={none || busy}
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
{deleteLabel}
|
|
236
|
-
</button>
|
|
296
|
+
destructive={deletesPermanently}
|
|
297
|
+
hint={hint(deleteLabel, SHORTCUTS.mail.delete)}
|
|
298
|
+
/>
|
|
237
299
|
</div>
|
|
238
300
|
</div>
|
|
239
301
|
);
|
|
@@ -830,6 +830,7 @@ function MessageDetailContent({
|
|
|
830
830
|
mailboxUid: message.mailboxUid,
|
|
831
831
|
subject: forwardSubject(message.subject),
|
|
832
832
|
signatureContext: "reply_forward",
|
|
833
|
+
inlineFor: message.uid,
|
|
833
834
|
encrypt,
|
|
834
835
|
threading,
|
|
835
836
|
pending: late,
|
|
@@ -843,6 +844,8 @@ function MessageDetailContent({
|
|
|
843
844
|
subject: replySubject(message.subject),
|
|
844
845
|
signatureContext: "reply_forward",
|
|
845
846
|
suppressSigning: isLikelyMailingList({ listUnsubscribe: message.listUnsubscribeHeader }),
|
|
847
|
+
// A card above this message in the reading pane when it is in one, else the floating window (see `OpenComposeInput.inlineFor`).
|
|
848
|
+
inlineFor: message.uid,
|
|
846
849
|
encrypt,
|
|
847
850
|
threading,
|
|
848
851
|
pending: late,
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { HTMLAttributes, ReactNode, useEffect, useRef, useState } from "react";
|
|
5
|
+
import React, { HTMLAttributes, ReactNode, TouchEvent, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { HiOutlineArchiveBox, HiOutlineFolder } from "react-icons/hi2";
|
|
7
|
+
import { useLongPress } from "../../gestures/useLongPress.js";
|
|
7
8
|
import { useSwipe } from "../../gestures/useSwipe.js";
|
|
8
9
|
|
|
9
10
|
export interface SwipeRowProps extends HTMLAttributes<HTMLElement> {
|
|
@@ -15,12 +16,22 @@ export interface SwipeRowProps extends HTMLAttributes<HTMLElement> {
|
|
|
15
16
|
onArchive: () => Promise<boolean>;
|
|
16
17
|
/** Left to right. The row comes back at once, and this asks where to (a prompt for the folder). */
|
|
17
18
|
onMove: () => void;
|
|
19
|
+
/** A finger held on the row (see `useLongPress()`) - the phone's way into select mode. Heard whether or not swipes are `enabled`; absent, the row has none. */
|
|
20
|
+
onLongPress?: () => void;
|
|
18
21
|
children: ReactNode;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/** How long the row takes to leave, or come back - `motion-reduce` skips it. */
|
|
22
25
|
const SLIDE = "transform 150ms ease-out";
|
|
23
26
|
|
|
27
|
+
/** Both hooks' handlers for one touch event; `useSwipe()` has none while swipes are off. */
|
|
28
|
+
function both(first: (event: TouchEvent<HTMLElement>) => void, second?: (event: TouchEvent<HTMLElement>) => void) {
|
|
29
|
+
return (event: TouchEvent<HTMLElement>) => {
|
|
30
|
+
first(event);
|
|
31
|
+
second?.(event);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* A row of a list of mail that slides with a finger: right to left it takes the row away to Archive (a green panel with the
|
|
26
37
|
* archive icon follows the row in), left to right it comes back and asks for a folder to move it to (a blue panel with a
|
|
@@ -29,8 +40,10 @@ const SLIDE = "transform 150ms ease-out";
|
|
|
29
40
|
*
|
|
30
41
|
* Only a swipe that starts out horizontal counts (see `useSwipe()`), so scrolling the list is untouched, and it is an addition
|
|
31
42
|
* to, never the only way of, archiving and moving: the selection bar and the reading pane do the same.
|
|
43
|
+
*
|
|
44
|
+
* The row is also where a press and hold is heard (`onLongPress`), which shares the finger with the swipe: a hold gives way to the first drag.
|
|
32
45
|
*/
|
|
33
|
-
export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove, children, className, style, ...rest }: SwipeRowProps) {
|
|
46
|
+
export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove, onLongPress, children, className, style, ...rest }: SwipeRowProps) {
|
|
34
47
|
const [offset, setOffset] = useState(0);
|
|
35
48
|
const [width, setWidth] = useState(0);
|
|
36
49
|
/** The row is on its way out (or waiting for the server to confirm it), so a finger can't drag it. */
|
|
@@ -71,9 +84,13 @@ export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove,
|
|
|
71
84
|
},
|
|
72
85
|
});
|
|
73
86
|
|
|
87
|
+
// What `useSwipe()` hands over is `{}` while swipes are off.
|
|
88
|
+
const swipeTouch: Partial<ReturnType<typeof useLongPress>["handlers"]> = swipe.handlers;
|
|
89
|
+
const press = useLongPress({ enabled: !!onLongPress, onLongPress: onLongPress! });
|
|
90
|
+
|
|
74
91
|
if (!enabled) {
|
|
75
92
|
return (
|
|
76
|
-
<Tag className={className} style={style} {...rest}>
|
|
93
|
+
<Tag className={className} style={{ ...style, ...press.style }} {...rest} {...press.handlers}>
|
|
77
94
|
{children}
|
|
78
95
|
</Tag>
|
|
79
96
|
);
|
|
@@ -82,7 +99,11 @@ export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove,
|
|
|
82
99
|
return (
|
|
83
100
|
<Tag
|
|
84
101
|
{...rest}
|
|
85
|
-
{...
|
|
102
|
+
{...press.handlers}
|
|
103
|
+
onTouchStart={both(press.handlers.onTouchStart, swipeTouch.onTouchStart)}
|
|
104
|
+
onTouchMove={both(press.handlers.onTouchMove, swipeTouch.onTouchMove)}
|
|
105
|
+
onTouchEnd={both(press.handlers.onTouchEnd, swipeTouch.onTouchEnd)}
|
|
106
|
+
onTouchCancel={both(press.handlers.onTouchCancel, swipeTouch.onTouchCancel)}
|
|
86
107
|
ref={(node: HTMLElement | null) => {
|
|
87
108
|
element.current = node;
|
|
88
109
|
}}
|
|
@@ -90,6 +111,7 @@ export default function SwipeRow({ as: Tag = "div", enabled, onArchive, onMove,
|
|
|
90
111
|
style={{
|
|
91
112
|
...style,
|
|
92
113
|
...swipe.style,
|
|
114
|
+
...press.style,
|
|
93
115
|
transform: active ? `translateX(${offset}px)` : undefined,
|
|
94
116
|
transition: dragging ? "none" : SLIDE,
|
|
95
117
|
}}
|