fix(forward): harden comment retry + a11y after review
CI / Build & Quality Checks (push) Successful in 10m47s
CI / Trigger Desktop Build (push) Successful in 9s

Address findings from 2 review agents on the forward upgrades:

- Duplicate comment on retry (correctness): if a room's comment message
  sent but the forward then failed, retrying re-posted the comment. Track
  rooms whose comment already delivered (commentSentRef) and skip it on
  retry, sending only the missing forward. An already-commented room won't
  get the comment again even if the text is later edited (no-duplicate
  choice).

- a11y: give the message-preview box role="group" + aria-label
  ("Message to forward"), add aria-label to the comment and search inputs
  (placeholder is not a label), and match the RecentChip's RoomIcon
  fallback size (100) to the room-row convention for a size-200 avatar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 00:43:32 -04:00
co-authored by Claude Opus 4.8
parent 629db9724f
commit 57f21e5cac
@@ -144,6 +144,8 @@ function ForwardPreview({
shrink="No"
gap="200"
alignItems="Center"
role="group"
aria-label="Message to forward"
style={{
margin: `${config.space.S200} ${config.space.S400} 0`,
padding: config.space.S200,
@@ -224,7 +226,7 @@ function RecentChip({
src={avatarUrl}
alt={room.name}
renderFallback={() => (
<RoomIcon roomType={room.getType()} size="50" joinRule={room.getJoinRule()} filled />
<RoomIcon roomType={room.getType()} size="100" joinRule={room.getJoinRule()} filled />
)}
/>
</Avatar>
@@ -254,6 +256,11 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const [sentTo, setSentTo] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [recents, setRecents] = useAtom(recentForwardTargetsAtom);
// Rooms whose comment message already delivered this session — so a retry after
// a forward failure doesn't re-post the comment (only the missing forward). Kept
// for the dialog's lifetime: a room that already got the comment won't get it
// again even if the text is later edited, which is the safe (no-duplicate) choice.
const commentSentRef = useRef<Set<string>>(new Set());
// Selection persists across query changes: a room selected then filtered out
// of the rendered slice stays selected.
const [selectedRoomIds, setSelectedRoomIds] = useState<Set<string>>(new Set());
@@ -315,12 +322,18 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
const sendForward = () => mx.sendEvent(id, null, mEvent.getType() as any, fwdContent);
// Send the optional comment first so it reads as a note above the
// forwarded content. The room counts as failed if either send rejects.
return commentBody
// Track rooms whose comment already landed so a retry (after the FORWARD
// failed) doesn't post the comment twice — only the missing forward.
const needsComment = commentBody && !commentSentRef.current.has(id);
const step = needsComment
? mx
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.sendMessage(id, null, { msgtype: MsgType.Text, body: commentBody } as any)
.then(sendForward)
: sendForward();
.then(() => {
commentSentRef.current.add(id);
})
: Promise.resolve();
return step.then(sendForward);
}),
);
@@ -408,6 +421,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
size="400"
radii="400"
outlined
aria-label="Search rooms"
placeholder="Search rooms…"
value={query}
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
@@ -417,6 +431,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
size="400"
radii="400"
outlined
aria-label="Add a comment"
placeholder="Add a comment (optional)…"
value={comment}
disabled={sending}