第四轮深度测试:修复7个agent发现的问题,重点是编辑态附件覆盖的完整旁路和通话面板崩溃防护
再次派7个agent覆盖全部11章,验证第三轮修复+找新问题。本次委托agent专门交叉核对 上一轮每一项修复是否真的生效,多数确认有效,但抓出2处"看似修好、实则有旁路"的情况 和若干新问题。 **编辑消息+拖放文件仍可静默覆盖被编辑消息(上一轮修复的完整旁路,Ch3-4 agent发现)** 上一轮给`onAttachFile`/`onAttachImage`/`onAttachAudio`三个prop加了editingMessage门禁, 但`isDragEnabled()`(messages-view.jsx)没有检查这个状态——拖放文件到聊天窗口走的是 `handleFileDrop`直接调用`handleAttachImageOrVideo`/`handleAttachFile`类方法本身,完全 绕开了被置null的props。编辑态下不点"+"号、直接把文件拖进聊天窗口,依然会静默替换掉 被编辑消息的原文。已给`isDragEnabled()`补上同款判断,这是`handleDrop`和渲染层拖放提示 两处调用的唯一门禁函数,一次性堵住。 同一个agent还发现编辑态下点击"回复"另一条消息会静默放弃当前编辑(`handlePickReply` 覆盖`this.state.reply`但不清`contentToEdit`,发送时值走了`reply`分支而不是`editing` 分支,编辑框里的原文被当成对另一条消息的新回复发出去,原消息未被修改)——已在 `handleShowMessageContextMenu`里比照`menu_item_edit`的年龄过滤模式,编辑态下直接不 提供"回复"这个菜单项。 **实名认证驳回提醒的dismissKey修复上一轮实际没生效(Ch1-2 agent发现,已重新修复)** 上一轮把dismissKey从纯status改成"submission_id或status",但live验证发现 `bootstrap.security.realname`这个数据源根本不包含submission_id字段(后端serveIMBootstrap 的实现只有status/required/allow_skip/remind_after四个硬编码字段)——所以这个修复在 真实环境下永远退化回status,等于没有变化,两次独立驳回依然会互相误压制提醒。这次改为 额外调用专用的`GET /security/realname`接口(该接口的实现确实会返回submission_id), 用它的数据驱动dismissKey和提醒触发条件,bootstrap的字段只保留用于remind_after/ allow_skip这些它独有的字段。已用真实账号验证dismissKey现在正确从这个新数据源生成。 **呼叫面板未卸载防护补全到call-panel.jsx(Ch8 agent发现,同类问题上一轮只修了 audio-recorder.jsx)** `start()`/`unmuteVideo()`/`handleSwitchCameraClick()`三处的getUserMedia/ enumerateDevices异步链,结尾都会touch `this.localRef.current`/`this.state.localStream`, 但都没检查组件是否已经卸载——权限弹窗未决出结果时通话结束(呼出超时/收到挂断/对方 主动挂断),这些promise resolve后会在死实例上抛TypeError,被各自的catch吞掉后又会展示 一句过期的原始英文报错。三处都补了`this._unmounted`检查,拿到的媒体流/轨道直接停止 而不是继续使用。 **其余修复**:`call-message.jsx`补上和`call-status.jsx`一致的"进行中"状态文案(原来 通话进行中且消息还没有最终时长时显示"0:00"而不是"进行中",已用真实渲染验证过两个 组件行为不一致);`tinode-web.jsx`的`handleCallClose`补上重置`callSeq`(避免跨通话 残留旧值)+ 更新`armCallRingTimeout`注释准确描述它是后端30秒建立超时的二级兜底而非 主要防线(后端`call_establishment_timeout`默认30秒,已有独立机制处理"直接不接"的 常见场景);`REASON_MESSAGES`补充`API_KEY_REQUIRED`/`METHOD_NOT_ALLOWED`(此前记录过 一次真实的部署期API Key配置漂移事故,属于这两个reason的真实触发场景); `message-center-view.jsx`的`NOTIFICATION_TEXT`补充风险确认"取消"结果的文案(发消息 前风险提示点"取消发送"会产生这个系统通知,此前只收录了"继续");会话列表"置顶"从 只变背景色改为真正排到列表最前(原来只有CSS着色,没有任何排序逻辑,已用真实群聊 置顶验证过排序生效且系统消息/文件助手两个固定入口仍保持在最前);好友聊天设置页 置顶/免打扰开关在初始GET未完成前禁用(避免`version:null`打到后端400); `content-library-view.jsx`的`DownloadPage`修复带外附件下载使用未做域名归一化的裸 相对路径(生产环境webapp和文件域名跨域时会必现下载失败,已拆出`resolveFileUrl` 辅助函数只做URL拼装不做预先fetch,跟`getFileUrl`的实际下载区分开); `security-support-view.jsx`的意见反馈分类配置改用`useCachedData`获得重试能力(原来 独立实现的边车缓存逻辑没享受到本轮`use-cached-data.js`的重试修复)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
93b856aec8
commit
4a30cfa6b3
+38
-18
@@ -3456,6 +3456,7 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ rejectGroupJoinRequest: function() { return /* binding */ rejectGroupJoinRequest; },
|
||||
/* harmony export */ reopenMonthlyReport: function() { return /* binding */ reopenMonthlyReport; },
|
||||
/* harmony export */ requestVerificationCode: function() { return /* binding */ requestVerificationCode; },
|
||||
/* harmony export */ resolveFileUrl: function() { return /* binding */ resolveFileUrl; },
|
||||
/* harmony export */ resolveQRCode: function() { return /* binding */ resolveQRCode; },
|
||||
/* harmony export */ resolveTenant: function() { return /* binding */ resolveTenant; },
|
||||
/* harmony export */ respondMeeting: function() { return /* binding */ respondMeeting; },
|
||||
@@ -3517,6 +3518,8 @@ class ImApiError extends Error {
|
||||
}
|
||||
}
|
||||
const REASON_MESSAGES = {
|
||||
API_KEY_REQUIRED: '服务暂不可用,请刷新页面重试',
|
||||
METHOD_NOT_ALLOWED: '请求方式有误,请刷新页面重试',
|
||||
TENANT_UNAVAILABLE: '企业码不存在或已停用',
|
||||
TENANT_MISMATCH: '登录状态异常,请重新进入企业码',
|
||||
AUTH_FAILED: '账号或密码错误',
|
||||
@@ -4175,6 +4178,9 @@ async function fetchFileState(fileId, range = '') {
|
||||
function publicFileBase() {
|
||||
return fileBase();
|
||||
}
|
||||
function resolveFileUrl(fileId) {
|
||||
return fileId ? fileBase() + '/s/' + encodeURIComponent(fileId) : null;
|
||||
}
|
||||
const fileUrlCache = new Map();
|
||||
function getFileUrl(fileId) {
|
||||
if (!fileId) {
|
||||
@@ -4193,7 +4199,7 @@ function getFileUrl(fileId) {
|
||||
if (token) {
|
||||
headers.Authorization = 'Token ' + token;
|
||||
}
|
||||
const promise = fetch(fileBase() + '/s/' + encodeURIComponent(fileId), {
|
||||
const promise = fetch(resolveFileUrl(fileId), {
|
||||
headers
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
@@ -8298,12 +8304,14 @@ function Profile({
|
||||
}
|
||||
function Toggle({
|
||||
checked,
|
||||
onChange
|
||||
onChange,
|
||||
disabled
|
||||
}) {
|
||||
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
role: "switch",
|
||||
"aria-checked": checked,
|
||||
disabled: disabled,
|
||||
className: 'people-switch' + (checked ? ' checked' : ''),
|
||||
onClick: () => onChange(!checked),
|
||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("i", {}, void 0, false)
|
||||
@@ -8393,6 +8401,7 @@ function Settings({
|
||||
children: "置顶聊天"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Toggle, {
|
||||
checked: pinned,
|
||||
disabled: settingsVersion === null,
|
||||
onChange: value => {
|
||||
const prev = pinned;
|
||||
setPinned(value);
|
||||
@@ -8410,6 +8419,7 @@ function Settings({
|
||||
children: "消息免打扰"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Toggle, {
|
||||
checked: muted,
|
||||
disabled: settingsVersion === null,
|
||||
onChange: value => {
|
||||
const prev = muted;
|
||||
setMuted(value);
|
||||
@@ -9443,11 +9453,13 @@ function DownloadPage({
|
||||
setLoadError(true);
|
||||
return;
|
||||
}
|
||||
const direct = tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.getDownloadUrl(att);
|
||||
const url = direct && (direct.startsWith('blob:') || direct.startsWith('data:')) ? direct : (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_5__.resolveFileUrl)(direct && direct.slice(direct.lastIndexOf('/') + 1));
|
||||
setMeta({
|
||||
name: att.name,
|
||||
mime: att.mime,
|
||||
size: tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.getEntitySize(att),
|
||||
url: tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.getDownloadUrl(att)
|
||||
url
|
||||
});
|
||||
};
|
||||
if (favoriteId) {
|
||||
@@ -13200,18 +13212,20 @@ function MessageHome({
|
||||
const [toast, setToast] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||
const [bootstrap] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useCachedData)(myUserId ? 'bootstrap:' + myUserId : null, _lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getBootstrap, [myUserId]);
|
||||
const offline = view === 'messages-offline' || !!myUserId && connected === false;
|
||||
const conversations = entryConversations(bootstrap).concat(normalizeConversations(chatList, myUserId));
|
||||
const realConversations = normalizeConversations(chatList, myUserId).sort((a, b) => (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0));
|
||||
const conversations = entryConversations(bootstrap).concat(realConversations);
|
||||
const filtered = conversations.filter(item => `${item.title}${item.summary}`.toLowerCase().includes(query.trim().toLowerCase()));
|
||||
const frequent = frequentContacts(chatList);
|
||||
const realname = bootstrap && bootstrap.security && bootstrap.security.realname;
|
||||
const realnameMeta = bootstrap && bootstrap.security && bootstrap.security.realname;
|
||||
const [realnameStatus] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useCachedData)(myUserId ? 'realname-status:' + myUserId : null, _lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getRealnameStatus, [myUserId]);
|
||||
const [reminderDismissed, setReminderDismissed] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const dismissKey = myUserId && realname ? `realname-reminder-dismissed:${myUserId}:${realname.submission_id || realname.status}` : null;
|
||||
const dismissKey = myUserId && realnameStatus ? `realname-reminder-dismissed:${myUserId}:${realnameStatus.submission_id || realnameStatus.status}` : null;
|
||||
let lastDismissedAt = 0;
|
||||
try {
|
||||
lastDismissedAt = dismissKey ? Number(localStorage.getItem(dismissKey) || 0) : 0;
|
||||
} catch (_) {}
|
||||
const withinSnooze = lastDismissedAt > 0 && Date.now() - lastDismissedAt < parseRemindAfter(realname && realname.remind_after);
|
||||
const showRealnameReminder = !!realname && !reminderDismissed && !withinSnooze && (realname.status === 'unverified' || realname.status === 'rejected');
|
||||
const withinSnooze = lastDismissedAt > 0 && Date.now() - lastDismissedAt < parseRemindAfter(realnameMeta && realnameMeta.remind_after);
|
||||
const showRealnameReminder = !!realnameStatus && !reminderDismissed && !withinSnooze && (realnameStatus.status === 'unverified' || realnameStatus.status === 'rejected');
|
||||
const dismissRealnameReminder = () => {
|
||||
if (dismissKey) {
|
||||
try {
|
||||
@@ -13331,7 +13345,7 @@ function MessageHome({
|
||||
message: toast,
|
||||
onDone: () => setToast('')
|
||||
}, void 0, false), showRealnameReminder ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)(RealnameReminderModal, {
|
||||
allowSkip: !!realname.allow_skip,
|
||||
allowSkip: !!(realnameMeta && realnameMeta.allow_skip),
|
||||
onDismiss: dismissRealnameReminder
|
||||
}, void 0, false) : null]
|
||||
}, void 0, true);
|
||||
@@ -13450,7 +13464,8 @@ const NOTIFICATION_TEXT = {
|
||||
'group.settings.updated': '群设置有更新',
|
||||
accepted: '对方已通过你的好友申请',
|
||||
rejected: '对方拒绝了你的好友申请',
|
||||
continue: '你确认继续了一次安全风险提示'
|
||||
continue: '你确认继续了一次安全风险提示',
|
||||
cancel: '你取消了一次风险操作确认'
|
||||
};
|
||||
function systemNotificationText(item) {
|
||||
const summary = (item.summary || '').trim();
|
||||
@@ -16562,7 +16577,7 @@ function UserFeedback({
|
||||
onToast
|
||||
}) {
|
||||
const cachedFeedbackConfig = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_3__.readCachedData)('feedback-config');
|
||||
const [config, setConfig] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => cachedFeedbackConfig || null);
|
||||
const [config] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_3__.useCachedData)('feedback-config', _lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.getFeedbackConfig, []);
|
||||
const [category, setCategory] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => cachedFeedbackConfig && cachedFeedbackConfig.categories && cachedFeedbackConfig.categories[0] || '');
|
||||
const [content, setContent] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||
const [images, setImages] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
|
||||
@@ -16574,12 +16589,10 @@ function UserFeedback({
|
||||
const maxFiles = config && config.attachment_policy && config.attachment_policy.max_files || 5;
|
||||
const maxLength = config && config.max_length || 300;
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.getFeedbackConfig)().then(data => {
|
||||
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_3__.writeCachedData)('feedback-config', data);
|
||||
setConfig(data);
|
||||
setCategory(data.categories && data.categories[0] || 'other');
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
if (config && config.categories && config.categories.length && !category) {
|
||||
setCategory(config.categories[0]);
|
||||
}
|
||||
}, [config]);
|
||||
const CATEGORY_LABELS = {
|
||||
bug: '功能异常',
|
||||
suggestion: '产品建议',
|
||||
@@ -19539,7 +19552,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
this.setState({
|
||||
callTopic: undefined,
|
||||
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_NONE,
|
||||
callAudioOnly: undefined
|
||||
callAudioOnly: undefined,
|
||||
callSeq: undefined
|
||||
});
|
||||
}
|
||||
handleCallAccept(topicName, setCallTopic, callSeq) {
|
||||
@@ -23318,6 +23332,12 @@ class CallMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
}, void 0, false);
|
||||
break;
|
||||
}
|
||||
} else if (['accepted', 'started'].includes(this.props.callState) && !this.props.duration) {
|
||||
duration = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
id: "call_in_progress",
|
||||
defaultMessage: "in progress",
|
||||
description: "Label for call in progress"
|
||||
}, void 0, false);
|
||||
} else {
|
||||
duration = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)("span", {
|
||||
children: (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_2__.secondsToTime)(this.props.duration / 1000)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1011,6 +1011,9 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
}
|
||||
params.topicName = this.state.topic;
|
||||
let menuItems = messageSpecificMenuItems || [];
|
||||
if (this.state.reply && this.state.reply.editing) {
|
||||
menuItems = menuItems.filter(item => item !== 'menu_item_reply');
|
||||
}
|
||||
const topic = this.props.tinode.getTopic(params.topicName);
|
||||
if (topic) {
|
||||
const editWindow = this.state.messagePolicy ? this.state.messagePolicy.edit_window_seconds : 0;
|
||||
@@ -1721,7 +1724,8 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
topic.pinMessage(seq, false);
|
||||
}
|
||||
isDragEnabled() {
|
||||
return this.state.isWriter && !this.state.unconfirmed && !this.props.forwardMessage && !this.state.peerMessagingDisabled;
|
||||
const editingMessage = !!(this.state.reply && this.state.reply.editing);
|
||||
return this.state.isWriter && !this.state.unconfirmed && !this.props.forwardMessage && !this.state.peerMessagingDisabled && !editingMessage;
|
||||
}
|
||||
handleDragStart(e) {
|
||||
e.preventDefault();
|
||||
@@ -3608,6 +3612,7 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
favPicker: null
|
||||
});
|
||||
} else if (!prevProps.disabled && this.props.disabled && this.state.audioRec) {
|
||||
this.endHoldRecording();
|
||||
this.setState({
|
||||
audioRec: false
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -168,6 +168,7 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
topic.onInfo = this.previousOnInfo;
|
||||
this.stop();
|
||||
@@ -241,6 +242,10 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
return;
|
||||
}
|
||||
navigator.mediaDevices.getUserMedia(this.localStreamConstraints).then(stream => {
|
||||
if (this._unmounted) {
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
return;
|
||||
}
|
||||
if (!this.localStreamConstraints.video) {
|
||||
stream.addTrack(this.emptyVideoTrack());
|
||||
}
|
||||
@@ -582,13 +587,19 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
}).then(stream => {
|
||||
const track = stream.getVideoTracks()[0];
|
||||
stream.removeTrack(track);
|
||||
if (this._unmounted) {
|
||||
track.stop();
|
||||
return;
|
||||
}
|
||||
return this.replaceLocalVideoTrack(track);
|
||||
}).then(_ => {
|
||||
this.sendOnDataChannel(VIDEO_UNMUTED_EVENT);
|
||||
}).catch(err => this.handleGetUserMediaError(err)).finally(_ => {
|
||||
this.setState({
|
||||
videoToggleInProgress: false
|
||||
});
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
videoToggleInProgress: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
sendOnDataChannel(message) {
|
||||
@@ -625,11 +636,17 @@ class CallPanel extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
|
||||
}
|
||||
const track = stream.getVideoTracks()[0];
|
||||
stream.removeTrack(track);
|
||||
if (this._unmounted) {
|
||||
track.stop();
|
||||
return null;
|
||||
}
|
||||
return this.replaceLocalVideoTrack(track);
|
||||
}).catch(err => this.handleGetUserMediaError(err)).finally(_ => {
|
||||
this.setState({
|
||||
videoToggleInProgress: false
|
||||
});
|
||||
if (!this._unmounted) {
|
||||
this.setState({
|
||||
videoToggleInProgress: false
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user