@rapidmx/web-client 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +3 -0
  2. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +22 -10
  3. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +60 -0
  4. package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +108 -0
  5. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +25 -1
  6. package/apps/shared/components/calendar/CalendarSearchBox.tsx +83 -0
  7. package/apps/shared/components/calendar/EventMatchNav.tsx +72 -0
  8. package/apps/shared/components/calendar/EventModal.tsx +33 -12
  9. package/apps/shared/components/calendar/ListView.tsx +207 -0
  10. package/apps/shared/components/calendar/MonthView.tsx +14 -2
  11. package/apps/shared/components/calendar/SlideIn.tsx +47 -0
  12. package/apps/shared/components/calendar/SplitDayView.tsx +5 -0
  13. package/apps/shared/components/calendar/TimeGridView.tsx +23 -12
  14. package/apps/shared/components/calendar/activeOccurrence.ts +28 -0
  15. package/apps/shared/components/calendar/allDay.ts +16 -1
  16. package/apps/shared/components/calendar/calendarView.ts +41 -0
  17. package/apps/shared/components/layout/AppShell.tsx +13 -6
  18. package/apps/shared/components/mail/ConversationList.tsx +32 -6
  19. package/apps/shared/components/mail/ConversationThreadPane.tsx +15 -0
  20. package/apps/shared/components/mail/MailSelectionBar.tsx +117 -55
  21. package/apps/shared/components/mail/MessageDetailPane.tsx +3 -0
  22. package/apps/shared/components/mail/compose/ComposeContext.tsx +193 -35
  23. package/apps/shared/components/mail/compose/ComposeWindow.tsx +62 -11
  24. package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +21 -10
  25. package/apps/shared/components/mail/compose/RichTextEditor.tsx +2 -1
  26. package/apps/shared/components/mail/compose/quotedBody.ts +45 -2
  27. package/apps/shared/components/mail/labelMenu.tsx +4 -0
  28. package/apps/shared/components/mail/layout/MailShell.tsx +9 -2
  29. package/apps/shared/components/mail/reading/MessageBody.tsx +1 -1
  30. package/apps/shared/components/mail/reading/bodyContent.ts +15 -13
  31. package/apps/shared/components/mail/reading/bodyHtml.ts +12 -7
  32. package/apps/shared/gestures/useEnterSlide.ts +1 -1
  33. package/apps/shared/keyboard/keymap.ts +2 -0
  34. package/apps/shared/mail/rangeSelection.ts +45 -0
  35. package/apps/shared/mail/rowExit.ts +39 -0
  36. package/apps/shared/mail/usePermanentDelete.tsx +14 -7
  37. package/apps/www/calendar/index.tsx +251 -53
  38. package/apps/www/index.tsx +563 -376
  39. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +3 -3
  40. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +15 -9
  41. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +15 -0
  42. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +16 -0
  43. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +20 -0
  44. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +31 -0
  45. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +20 -1
  46. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +7 -0
  47. package/dist/apps/shared/components/calendar/CalendarSearchBox.d.ts +22 -0
  48. package/dist/apps/shared/components/calendar/CalendarSearchBox.js +24 -0
  49. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +17 -0
  50. package/dist/apps/shared/components/calendar/EventMatchNav.js +37 -0
  51. package/dist/apps/shared/components/calendar/EventModal.d.ts +17 -1
  52. package/dist/apps/shared/components/calendar/EventModal.js +13 -11
  53. package/dist/apps/shared/components/calendar/ListView.d.ts +30 -0
  54. package/dist/apps/shared/components/calendar/ListView.js +111 -0
  55. package/dist/apps/shared/components/calendar/MonthView.js +12 -3
  56. package/dist/apps/shared/components/calendar/SlideIn.d.ts +16 -0
  57. package/dist/apps/shared/components/calendar/SlideIn.js +32 -0
  58. package/dist/apps/shared/components/calendar/SplitDayView.js +5 -1
  59. package/dist/apps/shared/components/calendar/TimeGridView.js +11 -3
  60. package/dist/apps/shared/components/calendar/activeOccurrence.d.ts +15 -0
  61. package/dist/apps/shared/components/calendar/activeOccurrence.js +21 -0
  62. package/dist/apps/shared/components/calendar/allDay.d.ts +4 -0
  63. package/dist/apps/shared/components/calendar/allDay.js +14 -1
  64. package/dist/apps/shared/components/calendar/calendarView.d.ts +16 -0
  65. package/dist/apps/shared/components/calendar/calendarView.js +36 -0
  66. package/dist/apps/shared/components/layout/AppShell.js +8 -4
  67. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -3
  68. package/dist/apps/shared/components/mail/ConversationList.js +11 -3
  69. package/dist/apps/shared/components/mail/ConversationThreadPane.js +9 -1
  70. package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -4
  71. package/dist/apps/shared/components/mail/MessageDetailPane.js +3 -0
  72. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +31 -0
  73. package/dist/apps/shared/components/mail/compose/ComposeContext.js +123 -11
  74. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +6 -1
  75. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +37 -11
  76. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +5 -1
  77. package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +10 -6
  78. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +2 -1
  79. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +3 -0
  80. package/dist/apps/shared/components/mail/compose/quotedBody.js +43 -1
  81. package/dist/apps/shared/components/mail/labelMenu.d.ts +3 -1
  82. package/dist/apps/shared/components/mail/labelMenu.js +2 -2
  83. package/dist/apps/shared/components/mail/layout/MailShell.js +7 -1
  84. package/dist/apps/shared/components/mail/reading/MessageBody.js +1 -1
  85. package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +5 -1
  86. package/dist/apps/shared/components/mail/reading/bodyContent.js +11 -11
  87. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +3 -2
  88. package/dist/apps/shared/components/mail/reading/bodyHtml.js +8 -4
  89. package/dist/apps/shared/gestures/useEnterSlide.d.ts +2 -0
  90. package/dist/apps/shared/gestures/useEnterSlide.js +1 -1
  91. package/dist/apps/shared/keyboard/keymap.d.ts +12 -0
  92. package/dist/apps/shared/keyboard/keymap.js +2 -0
  93. package/dist/apps/shared/mail/rangeSelection.d.ts +20 -0
  94. package/dist/apps/shared/mail/rangeSelection.js +34 -0
  95. package/dist/apps/shared/mail/rowExit.d.ts +12 -0
  96. package/dist/apps/shared/mail/rowExit.js +33 -0
  97. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  98. package/dist/apps/shared/mail/usePermanentDelete.js +13 -8
  99. package/dist/apps/www/calendar/index.js +177 -18
  100. package/dist/apps/www/index.js +257 -93
  101. package/dist/lib/calendar/eventSearch.d.ts +42 -0
  102. package/dist/lib/calendar/eventSearch.js +152 -0
  103. package/dist/lib/components/navigation/BottomTabBar.d.ts +15 -3
  104. package/dist/lib/components/navigation/BottomTabBar.js +52 -15
  105. package/dist/lib/components/navigation/NavBadge.d.ts +13 -0
  106. package/dist/lib/components/navigation/NavBadge.js +18 -0
  107. package/dist/lib/mail/compose/quotedImages.d.ts +21 -0
  108. package/dist/lib/mail/compose/quotedImages.js +41 -0
  109. package/dist/lib/mail/inlineImages.d.ts +38 -0
  110. package/dist/lib/mail/inlineImages.js +90 -0
  111. package/dist/lib/mail/mailApi.d.ts +8 -1
  112. package/dist/lib/mail/mailApi.js +57 -5
  113. package/lib/calendar/eventSearch.ts +172 -0
  114. package/lib/components/navigation/BottomTabBar.tsx +107 -22
  115. package/lib/components/navigation/NavBadge.tsx +33 -0
  116. package/lib/mail/compose/quotedImages.ts +49 -0
  117. package/lib/mail/inlineImages.ts +104 -0
  118. package/lib/mail/mailApi.ts +64 -5
  119. package/package.json +1 -1
  120. package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +0 -36
  121. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +0 -10
  122. 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. */
@@ -47,9 +48,16 @@ export interface ConversationListProps {
47
48
  * conversation is what this list's rows are. A child row stays a plain "open this message" button - a
48
49
  * mixed conversation/message selection has no sensible bulk semantics (see `MailListToolbar`). */
49
50
  selectMode?: boolean;
50
- /** The `conversationId`s currently ticked. */
51
+ /** The `conversationRowKey()`s currently ticked. */
51
52
  selectedConversationIds?: Set<string>;
52
- onToggleSelected?: (conversation: ConversationSummary) => void;
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.
@@ -95,6 +103,8 @@ export default function ConversationList({
95
103
  selectMode,
96
104
  selectedConversationIds,
97
105
  onToggleSelected,
106
+ onSelectWithModifier,
107
+ exitingKeys,
98
108
  swipe,
99
109
  newestFirst,
100
110
  }: ConversationListProps) {
@@ -175,9 +185,16 @@ export default function ConversationList({
175
185
  // list itself is arranged in. Copied before reversing - the fetched array is cached.
176
186
  const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
177
187
  const panelId = `conversation-messages-${conversation.mailboxUid ? `${conversation.mailboxUid}-` : ""}${conversation.conversationId}`;
178
- const ticked = selectedConversationIds?.has(conversation.conversationId) ?? false;
188
+ const ticked = selectedConversationIds?.has(id) ?? false;
189
+ const exiting = exitingKeys?.has(id) ?? false;
179
190
  return (
180
- <li key={id}>
191
+ <li
192
+ key={id}
193
+ data-exiting={exiting ? "true" : undefined}
194
+ aria-hidden={exiting ? "true" : undefined}
195
+ inert={exiting}
196
+ className={exiting ? "pointer-events-none" : undefined}
197
+ >
181
198
  <SwipeRow
182
199
  as="div"
183
200
  enabled={!!swipe?.enabled && !selectMode}
@@ -193,7 +210,12 @@ export default function ConversationList({
193
210
  <input
194
211
  type="checkbox"
195
212
  checked={ticked}
196
- onChange={() => onToggleSelected?.(conversation)}
213
+ onChange={(event) =>
214
+ // The change of a checkbox is caused by a click (or the Space key, which makes one): its modifiers are that event's.
215
+ onSelectWithModifier && isSelectionClick(event.nativeEvent as MouseEvent)
216
+ ? onSelectWithModifier(conversation, event.nativeEvent as MouseEvent)
217
+ : onToggleSelected?.(conversation)
218
+ }
197
219
  aria-label={`Select conversation: ${conversation.subject || "(no subject)"}`}
198
220
  className="w-4 h-4 accent-primary"
199
221
  />
@@ -218,7 +240,11 @@ export default function ConversationList({
218
240
  <button
219
241
  type="button"
220
242
  data-row-open
221
- onClick={() => onOpenMessage(conversation, conversation.latestMessageUid)}
243
+ onClick={(event) =>
244
+ onSelectWithModifier && isSelectionClick(event)
245
+ ? onSelectWithModifier(conversation, event)
246
+ : onOpenMessage(conversation, conversation.latestMessageUid)
247
+ }
222
248
  className={["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" ")}
223
249
  >
224
250
  <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
- function actionClassName(destructive = false): string {
69
- return `px-2 py-1 rounded-md text-sm ${destructive ? "text-danger font-semibold" : "text-text"} hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent`;
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-2 px-3 py-1.5 bg-surface-alt">
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
- <button type="button" onClick={onSelectAll} disabled={allSelected || listedCount === 0} className={actionClassName()}>
157
- Select all
158
- </button>
159
- <button type="button" onClick={onClearSelection} disabled={selectedCount === 0} className={actionClassName()}>
160
- Clear
161
- </button>
162
- <button type="button" onClick={onCancel} className={`${actionClassName()} ml-auto`}>
163
- Cancel
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
- <button type="button" onClick={() => onSetRead(true)} disabled={none || busy} className={actionClassName()} {...hint("Mark read", SHORTCUTS.mail.markRead)}>
168
- Mark read
169
- </button>
170
- <button type="button" onClick={() => onSetRead(false)} disabled={none || busy} className={actionClassName()} {...hint("Mark unread", SHORTCUTS.mail.markUnread)}>
171
- Mark unread
172
- </button>
173
- <button type="button" onClick={() => onSetFlagged(true)} disabled={none || busy} className={actionClassName()} {...hint("Flag", SHORTCUTS.mail.flag)}>
174
- Flag
175
- </button>
176
- <button type="button" onClick={() => onSetFlagged(false)} disabled={none || busy} className={actionClassName()}>
177
- Unflag
178
- </button>
179
- <button
180
- type="button"
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
- title={archiveReason}
184
- className={actionClassName()}
185
- >
186
- Archive
187
- </button>
252
+ reason={archiveReason}
253
+ />
188
254
  <LabelMenuButton
189
255
  aria-label="Apply label"
190
256
  label="Apply label"
191
- className="text-sm"
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
- <button
211
- type="button"
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
- title={moveDisabledReason}
215
- className={actionClassName()}
216
- >
217
- Move to
218
- </button>
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
- title={junkReason}
224
- className={actionClassName()}
225
- >
226
- Report junk
227
- </button>
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
- className={actionClassName(deletesPermanently)}
233
- {...hint(deleteLabel, SHORTCUTS.mail.delete)}
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,