第七轮深度测试:修复7个agent发现的12处前端问题,重点是列表分页截断与呼叫/播放状态机

- 通话状态机(Ch8):canStartCall 补齐通话中重复发起的防护,callShouldStart 补齐放弃/失败路径的清理
- 分页截断(Ch5-6):新增 fetchAllPages 辅助函数,修复通讯录/黑名单/好友申请/群成员/管理员/禁言等
  11 处列表只读首页导致真实成员被遗漏的问题;群角色判断改用不受分页影响的 getGroup().my_role
- 消息与媒体(Ch3-4/Ch7/Ch9):录音超时污染时长、手动重试丢失 head 导致消息被错误分类、批量删除
  未取消上传中附件、语音播放器补齐同时只能播一条、workplace-collaboration-view 冗余重试请求
- 独立密码/青少年模式(Ch10-11):设置密码后按调用来源判断是否应自动开启青少年模式,避免关闭流程
  中途"忘记密码"被误当成开启;注销账号确认文案补充群主后果说明

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-05 12:18:25 +08:00
co-authored by Claude Sonnet 5
parent f42b8536e5
commit bede45fd7f
19 changed files with 281 additions and 90 deletions
+20 -1
View File
@@ -19,6 +19,11 @@ const THUMB_COLOR = '#1781d7'; // average between dark and light accent colors.
// Minimum number of amplitude bars to draw.
const MIN_PREVIEW_LENGTH = 16;
// 7.5 "列表中同时只能有一条语音播放":favorites/history 列表和聊天气泡里的每条语音各自是一个独立
// AudioPlayer 实例,彼此不知道对方的存在。Module-scoped(不是某个列表父组件的 state)所以跨 tab、
// 跨收藏/聊天记录也生效,不只是同一个列表内部。
let activeAudioPlayer = null;
const messages = defineMessages({
icon_title_play: {
id: 'icon_title_play',
@@ -69,6 +74,9 @@ class AudioPlayer extends React.PureComponent {
}
componentWillUnmount() {
if (activeAudioPlayer === this) {
activeAudioPlayer = null;
}
if (this.audioPlayer) {
this.audioPlayer.onloadedmetadata = null;
this.audioPlayer.ontimeupdate = null;
@@ -78,6 +86,14 @@ class AudioPlayer extends React.PureComponent {
}
}
// Called on the previously-playing instance by whichever other instance is about to start.
pauseForOtherPlayer() {
if (this.audioPlayer) {
this.audioPlayer.pause();
}
this.setState({playing: false});
}
componentDidUpdate(prevProps) {
if (this.props.src != prevProps.src) {
this.initAudio();
@@ -100,7 +116,10 @@ class AudioPlayer extends React.PureComponent {
});
this.audioPlayer.onended = _ => {
this.audioPlayer.currentTime = 0;
this.setState({playing: false, currentTime: secondsToTime(0, this.state.longMin)})
this.setState({playing: false, currentTime: secondsToTime(0, this.state.longMin)});
if (activeAudioPlayer === this) {
activeAudioPlayer = null;
}
}
}
+1 -1
View File
@@ -156,7 +156,7 @@ class AudioRecorder extends React.PureComponent {
if (duration > MAX_DURATION) {
this.startedOn = null;
this.mediaRecorder.pause();
this.durationMillis += Date.now() - this.startedOn;
this.durationMillis = duration;
this.setState({enabled: false, recording: false, duration: secondsToTime(this.durationMillis / 1000)});
}
+1
View File
@@ -196,6 +196,7 @@ class BaseChatMessage extends React.PureComponent {
seq: this.props.seq,
replace: this.props.edited ? parseInt(this.props.edited.split(':')[1]) : 0,
content: this.props.content,
head: this.props.head,
userFrom: this.props.userFrom,
userName: this.props.userName,
y: e.pageY,
+9
View File
@@ -608,6 +608,15 @@ class ContextMenu extends React.Component {
return;
}
const msg = topic.createMessage(params.content, false);
// createMessage only ever fills in head.mime for non-plain-text content -- the original
// message's x-im-type/reply/mentions/mention_all live in params.head (set by
// chat-message.jsx's handleContextClick from the props it was rendered with) and would
// otherwise be silently dropped, unlike the auto-retry path (messages-view.jsx's retrySend)
// which already forwards the original head. Same merge tinode-web.jsx's
// sendMessageToTopic does for a fresh send.
if (params.head) {
msg.head = Object.assign(msg.head || {}, params.head);
}
return topic.publishDraft(msg).catch(err => {
if (errorHandler) {
errorHandler(err.message, 'err');