feat(web): bulk forward/favorite in multi-select mode

Rounds out the multi-select bottom bar (README §3.6) with 转发/收藏 alongside
the existing 删除 -- reuses the single-message ForwardPickerModal/
favoriteMessages/forwardMessage machinery from the context-menu actions,
just applied to the whole selected set. One "转发" button (not separate
逐条/合并 buttons) matching the old app's real dialog-internal mode toggle,
which this app's simplified single-target picker doesn't model -- already
noted as a scope cut in ForwardPickerModal's own docstring.

Verified live: entering multi-select via a message's context menu shows
转发/收藏/删除, and bulk-favorite POSTs a real /v1/im/favorites/batch request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-08 02:51:29 +08:00
co-authored by Claude Sonnet 5
parent 03b8098b3c
commit d5440b962b
@@ -47,6 +47,7 @@ export function ConversationView() {
const [selectedSeqs, setSelectedSeqs] = useState<Set<number>>(new Set());
const [loadingOlder, setLoadingOlder] = useState(false);
const [recording, setRecording] = useState(false);
const [bulkForwardOpen, setBulkForwardOpen] = useState(false);
const [replyTo, setReplyTo] = useState<StoredMessage | null>(null);
const [forwardMsg, setForwardMsg] = useState<StoredMessage | null>(null);
const [emojiOpen, setEmojiOpen] = useState(false);
@@ -161,6 +162,22 @@ export function ConversationView() {
exitSelectMode();
};
const bulkFavorite = () => {
if (selectedSeqs.size === 0) return;
favoriteMessages([...selectedSeqs].map((seq) => ({ topic: topicId, seq }))).catch(() => {});
exitSelectMode();
};
const bulkForward = (targetTopic: string) => {
const target = tinode.getTopic(targetTopic);
for (const seq of selectedSeqs) {
const msg = topic.findMessage(seq);
if (msg) forwardMessage(topicId, msg, target);
}
setBulkForwardOpen(false);
exitSelectMode();
};
const handleAction = (action: MessageAction) => {
const msg = menu?.msg;
setMenu(null);
@@ -275,6 +292,24 @@ export function ConversationView() {
{selectMode ? (
<div className="flex h-18 flex-none items-center justify-around border-t border-border-panel bg-white">
<button
type="button"
onClick={() => setBulkForwardOpen(true)}
disabled={selectedSeqs.size === 0}
className="flex flex-col items-center gap-1 text-text-secondary disabled:opacity-40"
>
<Icon name="reply" className="text-[22px]" />
<span className="text-xs">转发</span>
</button>
<button
type="button"
onClick={bulkFavorite}
disabled={selectedSeqs.size === 0}
className="flex flex-col items-center gap-1 text-text-secondary disabled:opacity-40"
>
<Icon name="inventory_2" className="text-[22px]" />
<span className="text-xs">收藏</span>
</button>
<button type="button" onClick={bulkDelete} className="flex flex-col items-center gap-1 text-danger-text">
<Icon name="delete_outline" className="text-[22px]" />
<span className="text-xs">删除</span>
@@ -379,6 +414,8 @@ export function ConversationView() {
}}
/>
) : null}
{bulkForwardOpen ? <ForwardPickerModal onClose={() => setBulkForwardOpen(false)} onPick={bulkForward} /> : null}
</div>
);
}