Make whole message row tappable to toggle selection
The selection checkbox was the only clickable target in multi-select mode; tapping the rest of the row (bubble, avatar, timestamp) did nothing. Moves the toggle handler to the row container so tapping anywhere selects/deselects, matching the multi-select UX in every other list this app has (favorites, blocklist, etc). Nested controls that keep their own click behavior during selectMode (retry-failed-send, @mention profile card, quote-bar jump-to-message) would otherwise double-fire alongside the new row click -- retry stops propagation, mentions and the quote bar go inert instead (same treatment this file already gives the avatar/sender-name buttons during selectMode), so a tap only ever does the one thing: toggle the row. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
95c0677b55
commit
1cc6f5068a
@@ -144,8 +144,19 @@ export function MessageRow({
|
||||
.catch(() => onError?.('重试发送失败'));
|
||||
};
|
||||
|
||||
// In selectMode, the WHOLE row toggles selection (not just the checkbox) -- confirmed by the user
|
||||
// that tapping only the small checkbox target is the wrong UX for a bulk-select list. A recalled
|
||||
// message renders a spacer instead of a checkbox (see below) and must stay unselectable, so this
|
||||
// guards the same way.
|
||||
const rowSelectable = selectMode && !isRecalled(msg) && typeof msg.seq === 'number';
|
||||
const toggleThisRow = () => rowSelectable && onToggleSelect?.(msg.seq as number);
|
||||
|
||||
return (
|
||||
<div data-seq={msg.seq} className={`flex items-center gap-2 ${highlighted ? 'animate-[jump-highlight_2s_ease-out]' : ''}`}>
|
||||
<div
|
||||
data-seq={msg.seq}
|
||||
onClick={toggleThisRow}
|
||||
className={`flex items-center gap-2 ${rowSelectable ? 'cursor-pointer' : ''} ${highlighted ? 'animate-[jump-highlight_2s_ease-out]' : ''}`}
|
||||
>
|
||||
{selectMode ? (
|
||||
// A recalled message (content nulled out, rendered as the "⚠ 消息已删除" placeholder below)
|
||||
// has nothing real to forward/favorite -- an empty flex-none spacer of the same size keeps
|
||||
@@ -154,9 +165,11 @@ export function MessageRow({
|
||||
isRecalled(msg) ? (
|
||||
<div className="h-5 w-5 flex-none" />
|
||||
) : (
|
||||
<button type="button" onClick={() => typeof msg.seq === 'number' && onToggleSelect?.(msg.seq)} className="flex-none">
|
||||
// No onClick of its own -- the row-level handler above already toggles it, and a second
|
||||
// handler here would double-toggle on every click (select then immediately un-select).
|
||||
<div className="flex-none">
|
||||
<SelectionCheckbox selected={selected} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
<div
|
||||
@@ -213,7 +226,7 @@ export function MessageRow({
|
||||
} ${isRecalled(msg) ? 'text-text-tertiary-4 italic' : ''} ${menuActive ? 'outline outline-2 outline-offset-2 outline-accent' : ''}`}
|
||||
>
|
||||
{replyInfo ? (
|
||||
<QuoteBar topicName={topic.name} reply={replyInfo} resolveName={resolveQuotedName ?? (() => '消息')} />
|
||||
<QuoteBar topicName={topic.name} reply={replyInfo} resolveName={resolveQuotedName ?? (() => '消息')} locked={selectMode} />
|
||||
) : null}
|
||||
{isRecalled(msg) ? (
|
||||
'⚠ 消息已删除'
|
||||
@@ -235,12 +248,19 @@ export function MessageRow({
|
||||
<FileAttachment data={mobileAttachment} />
|
||||
)
|
||||
) : (
|
||||
renderDrafty(msg.content)
|
||||
renderDrafty(msg.content, undefined, !selectMode)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{failed ? (
|
||||
<button type="button" onClick={retry} className="text-xs text-unread">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
retry();
|
||||
}}
|
||||
className="text-xs text-unread"
|
||||
>
|
||||
发送失败,点击重试
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -76,9 +76,14 @@ interface QuoteBarProps {
|
||||
/** Resolves a quoted sender uid to a display label (group remark/nick -> peer name); the parent
|
||||
* already owns this lookup, and must never surface a raw uid to the user. */
|
||||
resolveName: (uid: string | undefined) => string;
|
||||
/** MessageBubble's selectMode: a tap here must only toggle the row's own selection (same reasoning
|
||||
* as the avatar/sender-name buttons going inert during bulk-select), not also jump to the quoted
|
||||
* message -- so `locate` is left unwired and this renders as a plain, non-interactive div instead
|
||||
* of `role="button"`. */
|
||||
locked?: boolean;
|
||||
}
|
||||
|
||||
export function QuoteBar({ topicName, reply, resolveName }: QuoteBarProps) {
|
||||
export function QuoteBar({ topicName, reply, resolveName, locked }: QuoteBarProps) {
|
||||
const messages = useTopicMessages(topicName);
|
||||
const quoted: StoredMessage | undefined = useMemo(
|
||||
() => messages.find((m) => typeof m.seq === 'number' && m.seq === reply.seq),
|
||||
@@ -108,9 +113,9 @@ export function QuoteBar({ topicName, reply, resolveName }: QuoteBarProps) {
|
||||
if (deleted) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={locate}
|
||||
role={locked ? undefined : 'button'}
|
||||
tabIndex={locked ? undefined : 0}
|
||||
onClick={locked ? undefined : locate}
|
||||
className="mb-1.5 truncate rounded border-l-2 border-text-tertiary-3 bg-black/[0.04] px-2 py-1 text-xs italic text-text-tertiary-3"
|
||||
>
|
||||
该消息已删除
|
||||
@@ -120,10 +125,10 @@ export function QuoteBar({ topicName, reply, resolveName }: QuoteBarProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={locate}
|
||||
title="查看原消息"
|
||||
role={locked ? undefined : 'button'}
|
||||
tabIndex={locked ? undefined : 0}
|
||||
onClick={locked ? undefined : locate}
|
||||
title={locked ? undefined : '查看原消息'}
|
||||
className="mb-1.5 flex max-w-full items-center gap-1.5 rounded border-l-2 border-accent bg-black/[0.04] px-2 py-1 text-xs text-[#4a4f57]"
|
||||
>
|
||||
<span className="max-w-[40%] flex-none truncate text-accent">{name}</span>
|
||||
|
||||
Reference in New Issue
Block a user