实现合并转发(第4章 4.20):后端已支持 bundle 模式,补上前后两端
后端把 POST /v1/im/messages/forward 的 mode="bundle" 实现了(此前直接返回 501,前端因此
只做了逐条转发)。这次把前端两侧都补上。
接收侧(新增 widgets/forward-bundle.jsx)
- 服务端生成的这条消息 head 带 x-im-type:"forward_bundle",content 是
{bundle_id, title, item_count, preview, items, source_refs, comment},不是 Drafty。
前端全仓搜这个类型零命中,收到会落到 chat-message 的 "invalid content" 兜底
(⚠ + 灰字)。本端自己发不出 bundle,但别的端发过来就会踩到。
- 渲染成一张"聊天记录"卡片:标题 + 摘要行 + 展开/收起。items[] 里已经带了每条被转发消息的
完整快照,所以"查看完整记录"是就地展开,不额外发请求。
- 快照里的 from 是不带 usr 前缀的裸 uid(与收藏快照一致),补前缀后查本地会话缓存拿显示名;
查不到就不显示发送者,宁可少一行也不要把一串 uid 摆在摘要前面。
- 没有正文的媒体消息用 [图片]/[视频]/[语音]/[文件]/[链接] 标签兜底。
发送侧(batch-forward-dialog.jsx)
- 转发模式改为按"用哪种方式选的目标"推导,这是参考图给的规则:
转发—逐条转发.png 是单选一个目标、占位写 [逐条转发]共2条消息;
转发—合并转发—多选状态.png 是多选目标、占位写 [合并转发]共16条消息。
原来 mode 硬编码 'separate'、占位也写死 [逐条转发]。
样式(base.css)
- 新增 .forward-bundle 一组规则,避免又造一批零样式类。
验证
- 用后端 buildForwardBundle 的真实产出结构,注入真实会话的消息缓存,由真正的 ChatMessage
渲染:卡片正常显示、未落到 invalid content;展开显示全部 5 条、收起回到 3 条;截图确认视觉正常。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c709abbfa4
commit
5bc3fee20b
@@ -108,6 +108,14 @@ export default class BatchForwardDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// 转发模式由"用哪种方式选的目标"决定,这是参考图给的规则:
|
||||
// 转发—逐条转发.png 右上"多选"未点,单选一个目标,占位写 [逐条转发]共2条消息
|
||||
// 转发—合并转发—多选状态.png 右上已切"确定",勾选多个目标,占位写 [合并转发]共16条消息
|
||||
// 服务端两种 mode 现在都支持了(bundle 此前返回 501,已实现)。
|
||||
forwardMode() {
|
||||
return this.state.multiSelect ? 'bundle' : 'separate';
|
||||
}
|
||||
|
||||
handleCancelConfirm() {
|
||||
this.setState({confirmTargets: null, error: null});
|
||||
}
|
||||
@@ -126,7 +134,7 @@ export default class BatchForwardDialog extends React.Component {
|
||||
return;
|
||||
}
|
||||
this.setState({sending: true, error: null});
|
||||
forwardMessages('separate', sources, this.state.confirmTargets, this.state.comment.trim()).then(result => {
|
||||
forwardMessages(this.forwardMode(), sources, this.state.confirmTargets, this.state.comment.trim()).then(result => {
|
||||
const failed = (result && result.failed) || [];
|
||||
if (failed.length) {
|
||||
this.setState({sending: false, error: `${failed.length} 条转发失败,请重试`});
|
||||
@@ -155,7 +163,7 @@ export default class BatchForwardDialog extends React.Component {
|
||||
"附加留言"根本没法填,comment 永远是空串。方括号那段是占位文案,不是内容。 */}
|
||||
<input className="batch-forward-comment" type="text" value={this.state.comment}
|
||||
onChange={this.handleCommentChange} disabled={this.state.sending}
|
||||
placeholder={'[逐条转发]共' + this.props.seqs.length + '条消息'} />
|
||||
placeholder={(this.forwardMode() === 'bundle' ? '[合并转发]共' : '[逐条转发]共') + this.props.seqs.length + '条消息'} />
|
||||
{this.state.error ? <div className="auth-field-error">{this.state.error}</div> : null}
|
||||
<div className="batch-forward-actions">
|
||||
<button type="button" onClick={this.handleCancelConfirm} disabled={this.state.sending}>取消</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { Drafty, Tinode, TheCard } from 'tinode-sdk';
|
||||
|
||||
import Attachment from './attachment.jsx';
|
||||
import ForwardBundle from './forward-bundle.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
|
||||
@@ -229,6 +230,14 @@ class BaseChatMessage extends React.PureComponent {
|
||||
content && typeof content == 'object' && typeof content.text == 'string') {
|
||||
content = content.text;
|
||||
}
|
||||
// 合并转发(服务端 mode="bundle" 生成的消息)同样不是 Drafty,content 是
|
||||
// {bundle_id, title, item_count, preview, items, source_refs, comment}。不认这个类型的话
|
||||
// 一样会落到下面的 "invalid content" 兜底。渲染成一张可展开的"聊天记录"卡片。
|
||||
let bundleCard = null;
|
||||
if (this.props.head && this.props.head['x-im-type'] === 'forward_bundle' &&
|
||||
content && typeof content == 'object' && (content.items || content.preview)) {
|
||||
bundleCard = <ForwardBundle content={content} tinode={this.props.tinode} />;
|
||||
}
|
||||
const attachments = [];
|
||||
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
|
||||
Drafty.attachments(content, (att, i) => {
|
||||
@@ -288,7 +297,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
<div className={bubbleClass}>
|
||||
<div className="content-meta">
|
||||
<div className={textSizeClass}>
|
||||
{content}
|
||||
{bundleCard || content}
|
||||
{attachments}
|
||||
</div>
|
||||
{this.props.timestamp ?
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 合并转发卡片(第4章 4.20)。
|
||||
//
|
||||
// 服务端在 `POST /v1/im/messages/forward` 的 mode="bundle" 下会生成一条消息,
|
||||
// head 带 `x-im-type: "forward_bundle"`,content 形如:
|
||||
// {bundle_id, title, item_count, preview:[{from,type,summary,ts}], items:[...], source_refs, comment}
|
||||
// 这个结构不是 Drafty,所以不加这个渲染的话会落到 chat-message 的 "invalid content" 兜底。
|
||||
//
|
||||
// items[] 里已经带了每条被转发消息的完整快照,所以"查看完整记录"是就地展开,不需要额外请求。
|
||||
import React from 'react';
|
||||
|
||||
// 快照里的 from 是不带 usr 前缀的裸 uid(与收藏快照一致)。先补前缀再查本地会话缓存拿显示名,
|
||||
// 查不到就不显示发送者——宁可少一行,也不要把一串 uid 摆在摘要前面。
|
||||
function senderName(tinode, from) {
|
||||
const raw = (from || '').trim();
|
||||
if (!raw) return '';
|
||||
const uid = raw.startsWith('usr') ? raw : 'usr' + raw;
|
||||
const topic = tinode && tinode.getTopic ? tinode.getTopic(uid) : null;
|
||||
const pub = topic && topic.public;
|
||||
return (pub && (pub.fn || pub.name)) || '';
|
||||
}
|
||||
|
||||
const TYPE_LABEL = {image: '[图片]', video: '[视频]', voice: '[语音]', file: '[文件]', link: '[链接]'};
|
||||
|
||||
function lineText(entry) {
|
||||
if (!entry) return '';
|
||||
const summary = (entry.summary || entry.text || '').trim();
|
||||
if (summary) return summary;
|
||||
return TYPE_LABEL[entry.type] || '';
|
||||
}
|
||||
|
||||
export default class ForwardBundle extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {expanded: false};
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
}
|
||||
|
||||
handleToggle(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState(s => ({expanded: !s.expanded}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const c = this.props.content || {};
|
||||
const count = Number(c.item_count) || (Array.isArray(c.items) ? c.items.length : 0);
|
||||
// 收起时用 preview(服务端已截到最多 3 条);展开时用 items(全量)。
|
||||
const rows = this.state.expanded ? (c.items || []) : (c.preview || []);
|
||||
const comment = (c.comment || '').trim();
|
||||
|
||||
return <div className="forward-bundle">
|
||||
<div className="forward-bundle-title">{c.title || '聊天记录'}</div>
|
||||
<ul className="forward-bundle-lines">
|
||||
{rows.map((entry, i) => {
|
||||
const who = senderName(this.props.tinode, entry.from);
|
||||
return <li key={(entry.seq != null ? entry.seq : i) + ':' + i}>
|
||||
{who ? <b>{who}:</b> : null}{lineText(entry)}
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
{count > rows.length || this.state.expanded ?
|
||||
<button type="button" className="forward-bundle-more" onClick={this.handleToggle}>
|
||||
{this.state.expanded ? '收起' : `查看完整记录(共 ${count} 条)`}
|
||||
</button> :
|
||||
<div className="forward-bundle-count">共 {count} 条消息</div>}
|
||||
{comment ? <div className="forward-bundle-comment">{comment}</div> : null}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user