第七轮深度测试:修复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:
co-authored by
Claude Sonnet 5
parent
f42b8536e5
commit
bede45fd7f
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user