第三轮深度测试:修复7个并行agent发现的前端问题,含REASON_MESSAGES系统性补全

派了Ch1-2/3-4/5-6/7/8/9/10-11共7个agent对全部11章重新深度测试。后端问题已核实追加进
docs/IM-后端问题清单-2026-09-04.md(新增6-9条,发给用户);本次是前端问题的修复。

**REASON_MESSAGES系统性补全(最高优先级,多个agent独立发现同一个缺口)**
im-api.js的REASON_MESSAGES表新增22个条目,最重要的是PERMISSION_DENIED——后端
server/im_http.go里403响应统一用这个reason(30余处调用),但表里原来只有一个从未
被后端实际发送过的死条目FORBIDDEN,导致全站几乎每一个403从工程一开始就在展示原始
英文"permission denied"。一并补上VERSION_CONFLICT/MALFORMED_REQUEST(Ch9 agent实测
命中)、NOT_FOUND、VERIFICATION_RATE_LIMITED(动态文案,同TOO_MANY_ATTEMPTS模式)等。
新增translateReason()导出函数并用于send-message.jsx的sendFavorite——favorites/forward
等几个批量接口恒定200、真实成败包在items[].reason里,imRequest的自动翻译够不到这种
per-item场景,之前直接显示原始reason字符串。tinode-web.jsx同理补了CTRL_ERROR_TEXT_CN
(原生WS {ctrl}协议错误,同样绕开REASON_MESSAGES)并修了原表从未真正匹配过的bug——
CommError会把状态码拼进message变成"authentication failed (401)",原表按裸文本精确
匹配,看着正常纯粹是因为401/403兜底文案凑巧一样,一旦命中"unknown authentication
scheme"这类会被兜底文案误导、"policy violation"这类会直接漏出英文。uploadLargeFile/
createChatUploader/createFileDownloader同样绕开REASON_MESSAGES(走原生{ctrl}协议),
补了UPLOAD_CTRL_TEXT_CN小词典。已用真实账号端到端验证:favorites/forward到P2P目标
触发的PERMISSION_DENIED现在正确显示"没有权限执行此操作";draft接口版本冲突正确显示
"内容已被修改,请刷新后重试"。

**编辑消息时打开附件面板会静默覆盖被编辑消息(Ch3-4 agent发现,数据损坏级别)**
sendMessage对editing状态一律用head.replace,但onAttachFile/onAttachImage/onAttachAudio
原来只在forwardMessage时置空,编辑态没有同款门禁——编辑一条消息不改文字、点"+"选张图片
或名片,会直接把附件内容替换掉被编辑消息的原文,无任何确认。messages-view.jsx新增
editingMessage门禁,和forwardMessage一样把三个attach回调置空(等价于隐藏整个"+"按钮)。
已用真实浏览器验证:编辑态下"+"按钮消失,取消编辑后正常恢复。

**草稿防抖单一共享定时器丢字(Ch1-2、Ch3-4两个agent独立发现)**
handleComposerChange用单个this.draftSaveTimer,话题A打字触发600ms保存后300ms内切到
话题B,会把A的定时器clearTimeout掉,那次输入永久丢失;version也在定时器触发时才重新
读state,此时可能已经在别的话题上,退化成硬编码0——叠加后端draft接口对version=0"无条件
跳过乐观锁"的行为(已记入后端清单新增6),可能静默覆盖别的标签页写入的更新草稿。改为
按话题分开的定时器map,version在调用的当下(保证仍在该话题)同步捕获进闭包。

**呼叫状态机两处真实bug(Ch8 agent此前发现,本次重新核实live代码后确认+修复)**
1. 通话中导航离开messages-view.jsx整个组件树(切到工作台/个人设置,或移动端切回会话
   列表)时,没有任何hangup清理——componentSetup已有的"切换话题时正确挂断"逻辑只在
   componentDidUpdate里,组件真正卸载时不会触发。CallPanel自己的componentWillUnmount
   只做本地清理,不通知对方也不重置tinode-web.jsx顶层callState,导致下次切回通话话题
   时remount出一个callState卡在CALL_STATE_IN_PROGRESS的"僵尸"面板——start()走"我们刚
   接听"分支直接return,从不调getUserMedia,本地媒体永久拿不到。补了对称的
   componentWillUnmount挂断逻辑。
2. this.callTimeoutTimer声明后从未被真正setTimeout赋值——呼出无人接听或accept信令
   (fire-and-forget的{note}包)被丢会永久卡在"呼叫中"。补了armCallRingTimeout(),
   60秒未接通按跟手动取消完全一样的路径挂断(不发明新的通话结果语义),在收到对方
   accept信号的两处分支都正确清timer。

**其余前端修复(按文件)**
- messages-view.jsx: @提醒mentionedUsers只在切话题时清空,同一话题内后续消息里字面
  文本"@张三"会被静默解析成真实提醒(发送后清空);jumpToMessage点开已撤回/删除的
  搜索结果原来只console.error静默失败,补了用户可见的错误提示;批量收藏对
  favorites/batch共享Idempotency-Key导致的静默丢失(已知后端问题)不再照单全收
  "收藏成功",改为发送后重新拉取真实收藏列表核对每条是否真的落地。
- audio-recorder.jsx: getUserMedia权限弹窗未决出结果时组件被卸载,补_unmounted
  标记避免在死实例上继续跑mediaRecorder.start()。
- send-message.jsx: disabled变为true时(对方拉黑/被禁言等实时ACS变化)没有强制结束
  正在进行的按住录音,可能remount出无用户手势的"自动录音";mouseup/touchend覆盖不到
  拖出窗口/切换标签页/权限弹窗抢焦点的释放场景,补了blur/visibilitychange兜底。
- tinode-web.jsx: messages-menu视图和messages-home共用MessageCenterView实例无key,
  从messages-home以非整体刷新方式切到messages-menu时菜单打不开,补key={view}。
- message-center-view.jsx: 实名认证驳回提醒的dismiss key只按用户+状态区分,同一状态
  的两次独立驳回会互相误压制,改按submission_id区分。
- contact-management-view.jsx: Profile/Settings靠getUserProfile刷新拉黑状态,但该
  接口对被拉黑对象直接403,导致"之前会话就已拉黑"的场景完全感知不到——叠加
  getConversationContext兜底(该接口对拉黑目标正确返回200+relationship)。
- group-management-view.jsx: AdminList显示群主/管理员姓名漏了group_nickname兜底;
  JoinRequests从聊天页横幅进入时返回按钮硬编码回group-settings(应回聊天本身),
  从Governance进入时返回group-settings而非group-governance(与AdminList/Muted不一致)。
- workplace-collaboration-view.jsx: OrganizationDirectory/DeptNode的部门树加载失败
  被无条件当成"暂无数据"渲染,补loadError区分+重试链接。
- personal-settings-view.jsx: CacheClear对navigator.storage/caches整个不存在的情况
  只有react出了范围的.catch()覆盖不到的同步抛错,加了特性检测+try/catch;
  YouthModeVerify触发TOO_MANY_ATTEMPTS锁定后确定按钮没有禁用/倒计时,会继续发出
  注定失败的请求。
- security-support-view.jsx: 意见反馈category字段在config未加载完成前可为空但没有
  提交前校验,会打到后端400。
- use-cached-data.js: bootstrap等首次拉取失败且无缓存兜底时永久停在undefined,加了
  一次3秒后重试(仅在完全没有缓存值时触发,已有缓存值维持原"显示旧数据"行为不变)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-04 23:29:52 +08:00
co-authored by Claude Sonnet 5
parent 505a6d7d56
commit 93b856aec8
20 changed files with 662 additions and 140 deletions
+184 -35
View File
@@ -3476,6 +3476,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ submitTenantApplication: function() { return /* binding */ submitTenantApplication; },
/* harmony export */ submitTenantEntryFeedback: function() { return /* binding */ submitTenantEntryFeedback; },
/* harmony export */ transferGroupOwner: function() { return /* binding */ transferGroupOwner; },
/* harmony export */ translateReason: function() { return /* binding */ translateReason; },
/* harmony export */ unblockUser: function() { return /* binding */ unblockUser; },
/* harmony export */ unmuteGroupMember: function() { return /* binding */ unmuteGroupMember; },
/* harmony export */ updateConversationSettings: function() { return /* binding */ updateConversationSettings; },
@@ -3525,7 +3526,28 @@ const REASON_MESSAGES = {
VALIDATION_FAILED: '提交的内容不符合要求',
IDEMPOTENCY_CONFLICT: '内容已发生变化,请刷新后重试',
RESOURCE_NOT_FOUND: '内容不存在或已被删除',
FORBIDDEN: '没有权限执行此操作',
PERMISSION_DENIED: '没有权限执行此操作',
NOT_FOUND: '内容不存在或已被删除',
VERSION_CONFLICT: '内容已被修改,请刷新后重试',
MALFORMED_REQUEST: '请求内容有误,请稍后重试',
AUTH_REQUIRED: '请先登录',
ACTION_TOKEN_INVALID: '操作凭证已失效,请重新操作',
SCHEMA_UNSUPPORTED: '当前版本过旧,请刷新页面后重试',
IDEMPOTENCY_IN_PROGRESS: '上一次操作正在处理中,请稍后再试',
INTERNAL_ERROR: '服务器繁忙,请稍后重试',
UNSUPPORTED_MEDIA_TYPE: '不支持的文件类型',
CHALLENGE_EXPIRED: '验证码已过期,请重新获取',
SMS_DELIVERY_FAILED: '短信发送失败,请稍后重试',
PASSWORD_POLICY_VIOLATION: '密码不符合安全要求',
VERIFICATION_RATE_LIMITED: details => details && details.retry_after_seconds ? `操作过于频繁,请 ${details.retry_after_seconds} 秒后重试` : '操作过于频繁,请稍后再试',
ALREADY_GROUP_MEMBER: '对方已经是群成员',
DESCRIPTION_REQUIRED: '请填写简介内容',
DESCRIPTION_TOO_LONG: '简介内容过长',
GROUP_ADMIN_LIMIT_EXCEEDED: '群管理员数量已达上限',
INVALID_CREDIT_CODE: '统一社会信用代码格式不正确',
INVALID_EMAIL: '邮箱格式不正确',
MAILBOX_UNAVAILABLE: '总经理信箱暂不可用',
TOO_MANY_FILES: '文件数量超出限制',
FRIEND_RELATION_REQUIRED: '需要先添加对方为好友',
MESSAGE_NOT_ALLOWED: '当前状态无法发送消息',
GROUP_MUTED: '你已被禁言,无法发言',
@@ -3603,12 +3625,15 @@ async function imRequest(path, {
}
}
if (!response.ok || payload && payload.code && payload.code !== 0) {
const entry = payload.reason && REASON_MESSAGES[payload.reason];
const known = typeof entry === 'function' ? entry(payload.details) : entry;
const known = translateReason(payload.reason, payload.details);
throw new ImApiError(known || payload.message || payload.reason || response.statusText || '请求失败', payload.reason, response.status, payload.details);
}
return normalizeResponse(payload);
}
function translateReason(reason, details) {
const entry = reason && REASON_MESSAGES[reason];
return (typeof entry === 'function' ? entry(details) : entry) || null;
}
const idem = () => ({
'Idempotency-Key': requestId()
});
@@ -3675,6 +3700,12 @@ async function uploadPublicFile(file, purpose, onProgress) {
xhr.send(form);
});
}
const UPLOAD_CTRL_TEXT_CN = {
'too large': '文件大小超出限制',
'malformed': '请求内容有误,请稍后重试',
'permission denied': '没有权限执行此操作',
'gone': '内容不存在或已被删除'
};
function uploadLargeFile(tinode, file, onProgress) {
const authToken = tinode && tinode.getAuthToken && tinode.getAuthToken();
if (!authToken) {
@@ -3705,7 +3736,8 @@ function uploadLargeFile(tinode, file, onProgress) {
expires: ctrl.params.expires
});
} else {
reject(new ImApiError(ctrl.text || '上传失败', 'UPLOAD_FAILED', xhr.status));
const known = ctrl.text && UPLOAD_CTRL_TEXT_CN[ctrl.text.trim().toLowerCase()];
reject(new ImApiError(known || ctrl.text || '上传失败', 'UPLOAD_FAILED', xhr.status));
}
};
xhr.onerror = () => reject(new ImApiError('网络错误,上传失败', 'NETWORK_ERROR', 0));
@@ -3745,7 +3777,8 @@ function createChatUploader(tinode) {
if (xhr.status >= 200 && xhr.status < 300 && ctrl.params && ctrl.params.url) {
resolve(ctrl.params.url);
} else {
reject(new ImApiError(ctrl.text || '上传失败', 'UPLOAD_FAILED', xhr.status));
const known = ctrl.text && UPLOAD_CTRL_TEXT_CN[ctrl.text.trim().toLowerCase()];
reject(new ImApiError(known || ctrl.text || '上传失败', 'UPLOAD_FAILED', xhr.status));
}
};
xhr.onerror = () => reject(new ImApiError('网络错误,上传失败', 'NETWORK_ERROR', 0));
@@ -3804,7 +3837,9 @@ function createFileDownloader(tinode) {
let err;
try {
const pkt = JSON.parse(this.result);
err = new ImApiError(pkt.ctrl && pkt.ctrl.text || '下载失败', 'DOWNLOAD_FAILED', xhr.status);
const text = pkt.ctrl && pkt.ctrl.text;
const known = text && UPLOAD_CTRL_TEXT_CN[text.trim().toLowerCase()];
err = new ImApiError(known || text || '下载失败', 'DOWNLOAD_FAILED', xhr.status);
} catch (e) {
err = new ImApiError('下载失败', 'DOWNLOAD_FAILED', xhr.status);
}
@@ -4075,11 +4110,13 @@ const favoriteMessages = messages => imPost('/favorites/batch', {
const getFavorites = ({
type = 'all',
query = '',
cursor = ''
cursor = '',
limit
} = {}) => imGet('/favorites' + qs({
type,
q: query,
cursor
cursor,
limit
}));
const getFavorite = favoriteId => imGet('/favorites/' + encodeURIComponent(favoriteId));
const deleteFavorite = favoriteId => imDelete('/favorites/' + encodeURIComponent(favoriteId));
@@ -4734,15 +4771,25 @@ function useCachedData(key, fetcher, deps) {
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!key) return;
let live = true;
let retried = false;
setLoading(!cache.has(key));
fetcher().then(result => {
const attempt = () => fetcher().then(result => {
if (!live) return;
cache.set(key, result);
setData(result);
setLoading(false);
}).catch(() => {
if (live) setLoading(false);
if (!live) return;
if (!retried && !cache.has(key)) {
retried = true;
window.setTimeout(() => {
if (live) attempt();
}, 3000);
return;
}
setLoading(false);
});
attempt();
return () => {
live = false;
};
@@ -8137,6 +8184,14 @@ function Profile({
return current;
});
});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getConversationContext)(uid).then(ctx => {
if (ctx && ctx.relationship) {
setProfile(current => current ? {
...current,
relationship: ctx.relationship
} : current);
}
}).catch(() => {});
}, [uid]);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!uid) return undefined;
@@ -8279,7 +8334,15 @@ function Settings({
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + uid, p);
setProfile(p);
if (!pinBlocked) setBlocked(p.relationship === 'blocked_by_me');
}).catch(() => {});
}).catch(() => {
if (!pinBlocked) {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getConversationContext)(uid).then(ctx => {
if (ctx && ctx.relationship) {
setBlocked(ctx.relationship === 'blocked_by_me');
}
}).catch(() => {});
}
});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)().then(data => {
const entry = (data.items || []).find(item => item.user_id === uid);
if (entry) setRemark(entry.remark || '');
@@ -11043,8 +11106,12 @@ function GroupChat({
}
function JoinRequests({
topic,
fromChat,
onToast
}) {
const backToChat = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel('', ''), topic));
}, [topic]);
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => topic && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-join-requests:' + topic) || null);
const [profiles, setProfiles] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
@@ -11098,8 +11165,9 @@ function JoinRequests({
className: "group-page",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Header, {
title: "邀请进群申请",
back: "group-settings",
backTopic: topic
back: fromChat ? undefined : 'group-governance',
backTopic: topic,
onBack: fromChat ? backToChat : undefined
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
className: "group-request-list",
children: items === null ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
@@ -11896,7 +11964,7 @@ function AdminList({
item: owner
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
children: owner.display_name
children: owner.group_nickname || owner.display_name
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
children: "群主"
}, void 0, false)]
@@ -11912,7 +11980,7 @@ function AdminList({
item: item
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
children: item.display_name
children: item.group_nickname || item.display_name
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
children: "管理员"
}, void 0, false)]
@@ -12592,6 +12660,7 @@ function GroupManagementView({
onToast: setToast
}, void 0, false);else if (view === 'group-join-requests') content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(JoinRequests, {
topic: topic,
fromChat: hashParams && hashParams.from === 'chat',
onToast: setToast
}, void 0, false);else if (view === 'group-announcement-detail') content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Announcement, {
topic: topic,
@@ -13136,7 +13205,7 @@ function MessageHome({
const frequent = frequentContacts(chatList);
const realname = bootstrap && bootstrap.security && bootstrap.security.realname;
const [reminderDismissed, setReminderDismissed] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const dismissKey = myUserId ? 'realname-reminder-dismissed:' + myUserId : null;
const dismissKey = myUserId && realname ? `realname-reminder-dismissed:${myUserId}:${realname.submission_id || realname.status}` : null;
let lastDismissedAt = 0;
try {
lastDismissedAt = dismissKey ? Number(localStorage.getItem(dismissKey) || 0) : 0;
@@ -15347,16 +15416,22 @@ function CacheClear({
const confirm = async () => {
setBusy(true);
try {
const before = (await navigator.storage.estimate().catch(() => ({
const hasStorageEstimate = typeof navigator !== 'undefined' && navigator.storage && navigator.storage.estimate;
const hasCaches = typeof caches !== 'undefined';
const before = hasStorageEstimate ? (await navigator.storage.estimate().catch(() => ({
usage: 0
}))).usage || 0;
const keys = await caches.keys();
await Promise.all(keys.map(k => caches.delete(k)));
const after = (await navigator.storage.estimate().catch(() => ({
}))).usage || 0 : 0;
if (hasCaches) {
const keys = await caches.keys();
await Promise.all(keys.map(k => caches.delete(k)));
}
const after = hasStorageEstimate ? (await navigator.storage.estimate().catch(() => ({
usage: 0
}))).usage || 0;
}))).usage || 0 : 0;
setFreed(Math.max(0, before - after));
setDone(true);
} catch (err) {
onToast(err.message || '清理失败,请重试');
} finally {
setBusy(false);
}
@@ -15832,6 +15907,14 @@ function YouthModeVerify({
const [password, setPassword] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [error, setError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [busy, setBusy] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [lockedUntil, setLockedUntil] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(0);
const [now, setNow] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(Date.now());
const remainingLockSeconds = lockedUntil ? Math.max(0, Math.ceil((lockedUntil - now) / 1000)) : 0;
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (remainingLockSeconds <= 0) return;
const id = window.setTimeout(() => setNow(Date.now()), 1000);
return () => window.clearTimeout(id);
}, [remainingLockSeconds]);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_6__.getYouthMode)().then(setSettings).catch(() => {});
}, []);
@@ -15840,6 +15923,9 @@ function YouthModeVerify({
setError('请输入4位独立密码');
return;
}
if (remainingLockSeconds > 0) {
return;
}
setBusy(true);
setError('');
const action = mode === 'enable' ? 'enable_youth_mode' : 'close_youth_mode';
@@ -15855,7 +15941,13 @@ function YouthModeVerify({
});
onToast(mode === 'enable' ? '青少年模式已开启' : '青少年模式已关闭');
window.setTimeout(() => navigate(mode === 'enable' ? 'youth-mode-on' : 'youth-mode-off'), 400);
}).catch(err => setError(err.message || '验证失败')).finally(() => setBusy(false));
}).catch(err => {
if (err.reason === 'TOO_MANY_ATTEMPTS' && err.details && err.details.retry_after_seconds) {
setLockedUntil(Date.now() + err.details.retry_after_seconds * 1000);
setNow(Date.now());
}
setError(err.message || '验证失败');
}).finally(() => setBusy(false));
};
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
className: "personal-page youth-mode-page",
@@ -15885,9 +15977,9 @@ function YouthModeVerify({
}, void 0, false) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("button", {
type: "button",
className: "auth-primary",
disabled: password.length !== 4 || busy,
disabled: password.length !== 4 || busy || remainingLockSeconds > 0,
onClick: submit,
children: busy ? '验证中…' : '确定'
children: busy ? '验证中…' : remainingLockSeconds > 0 ? `请 ${remainingLockSeconds} 秒后重试` : '确定'
}, void 0, false)]
}, void 0, true)]
}, void 0, true);
@@ -16514,6 +16606,10 @@ function UserFeedback({
onToast('请填写反馈内容');
return;
}
if (!category) {
onToast('请选择反馈类型');
return;
}
setSubmitting(true);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.submitFeedback)({
category,
@@ -17013,7 +17109,7 @@ class SidepanelView extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
myUserId: this.props.myUserId,
onTopicSelected: this.props.onTopicSelected,
onOpenScanner: this.props.onOpenScanner
}, void 0, false)
}, view, false)
}, void 0, false);
}
const chapterThreeViews = ['chat-inbound-request', 'chat-inbound-message', 'chat-outbound-request', 'chat-status', 'chat-friend', 'chat-media', 'chat-blocked', 'chat-deleted', 'chat-standard', 'chat-attachments', 'chat-mixed', 'chat-contact-card'];
@@ -17450,18 +17546,26 @@ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
});
const SIDE_PANEL_VIEWS = ['acc_general', 'archive', 'blocked', 'contacts', 'cred', 'crop', 'edit', 'general', 'newtpk', 'notif', 'register', 'reset', 'security', 'settings', 'support', 'wallpapers', 'tenant', 'login', 'enterprise-register', 'enterprise-feedback', 'identity-check', 'realname', 'realname-verified', 'realname-reminder', 'independent-password', 'messages-home', 'messages-offline', 'messages-menu', 'message-actions', 'system-messages', 'chat-inbound-request', 'chat-inbound-message', 'chat-outbound-request', 'chat-status', 'chat-friend', 'chat-media', 'chat-blocked', 'chat-deleted', 'chat-standard', 'chat-attachments', 'chat-mixed', 'chat-contact-card', 'compose-keyboard', 'mentions-select', 'mentions-complete', 'mentions-message', 'emoji-panel', 'emoji-message', 'voice-ready', 'voice-hold', 'voice-recording', 'media-uploading', 'media-gallery', 'media-retry', 'upload-file', 'file-message', 'message-multiselect', 'favorite-success', 'forward-select', 'forward-multiple', 'forward-individual', 'forward-bundle', 'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', 'friend-qr', 'friend-search-result', 'friend-verification', 'friend-profile', 'group-member-profile', 'friend-settings', 'friend-settings-blocked', 'friend-delete-confirm', 'block-list', 'block-confirm', 'clear-chat-confirm', 'group-list', 'group-empty', 'group-invite', 'group-events', 'group-join-requests', 'group-new-request', 'group-announcement', 'group-summary', 'group-announcement-detail', 'group-settings', 'group-profile', 'group-avatar', 'group-nickname', 'group-description', 'group-my-nickname', 'group-role-message', 'group-qr', 'group-governance', 'group-admins', 'group-admin-add', 'group-admin-success', 'group-admin-partial', 'group-admin-removed', 'group-mute-add', 'group-muted', 'group-owner-select', 'group-owner-confirm', 'group-files', 'group-file-upload', 'favorites', 'favorites-mixed', 'favorites-files', 'favorites-text', 'favorites-voice', 'favorites-links', 'favorites-select', 'favorite-share', 'file-download', 'file-downloading', 'file-downloaded', 'history-search', 'history-files', 'history-links', 'history-images', 'history-videos', 'call-history', 'call-incoming', 'audio-call-ringing', 'audio-call-active', 'audio-call-muted', 'audio-call-speaker', 'video-call-ringing', 'video-call-camera-off', 'video-call-active', 'video-call-peer-camera-off', 'call-minimized', 'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-mine', 'monthly-report-team', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice', 'personal-center', 'personal-profile', 'profile-nickname', 'profile-birth-date', 'profile-signature', 'profile-gender', 'profile-region', 'personal-qrcode', 'android-settings', 'cache-clear-confirm', 'clear-history-confirm', 'change-password', 'delete-account-confirm', 'app-version', 'privacy-settings', 'friend-discovery-settings', 'youth-mode-off', 'youth-mode-on', 'youth-mode-verify', 'notification-permission', 'permission-settings-reminder', 'camera-permission-info', 'microphone-permission-info', 'fraud-risk-warning', 'fraud-risk-confirmation', 'abuse-report-reasons', 'abuse-report-details', 'user-feedback', ''];
const AUTH_FLOW_VIEWS = ['tenant', 'login', 'register', 'reset', 'cred', 'enterprise-register', 'enterprise-feedback'];
const LOGIN_ERROR_TEXT_CN = {
const CTRL_ERROR_TEXT_CN = {
'authentication failed': '账号或密码错误',
'authentication required': '请先登录',
'unknown authentication scheme': '登录方式不受支持,请刷新页面重试',
'policy violation': '当前操作不符合安全策略要求'
'policy violation': '当前操作不符合安全策略要求',
'permission denied': '没有权限执行此操作,请重新登录'
};
function translateCtrlError(err) {
const raw = err && err.message;
if (!raw) return null;
const normalized = raw.replace(/\s*\(\d+\)\s*$/, '').trim().toLowerCase();
return CTRL_ERROR_TEXT_CN[normalized] || null;
}
function loginErrorMessage(err) {
const known = err && err.message && LOGIN_ERROR_TEXT_CN[err.message.trim().toLowerCase()];
const known = translateCtrlError(err);
if (known) return known;
if (err && (err.code === 401 || err.code === 403)) return '账号或密码错误';
return err && err.message || '登录失败,请重试';
}
const CALL_RING_TIMEOUT_MS = 60000;
class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
constructor(props) {
super(props);
@@ -18086,11 +18190,12 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}, 1000);
}
handleDisconnect(err) {
const known = translateCtrlError(err);
this.setState({
connected: false,
ready: false,
topicSelectedOnline: false,
errorText: err && err.message ? err.message : "Disconnected",
errorText: known || (err && err.message ? err.message : "Disconnected"),
errorLevel: err && err.message ? 'err' : 'warn',
loginDisabled: false,
contextMenuVisible: false,
@@ -19336,6 +19441,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}, void 0, false), null, null, _ => {}, null);
}
handleStartVideoCall() {
this.armCallRingTimeout();
this.setState({
callTopic: this.state.topicSelected,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED,
@@ -19343,12 +19449,24 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
});
}
handleStartAudioCall() {
this.armCallRingTimeout();
this.setState({
callTopic: this.state.topicSelected,
callState: _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED,
callAudioOnly: true
});
}
armCallRingTimeout() {
clearTimeout(this.callTimeoutTimer);
this.callTimeoutTimer = setTimeout(() => {
this.callTimeoutTimer = null;
if (this.state.callState != _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED) {
return;
}
this.handleCallHangup(this.state.callTopic, this.state.callSeq);
this.handleCallClose();
}, CALL_RING_TIMEOUT_MS);
}
handleCallInvite(callTopic, callSeq, callState, audioOnly) {
switch (callState) {
case _constants_js__WEBPACK_IMPORTED_MODULE_13__.CALL_STATE_OUTGOING_INITATED:
@@ -19425,6 +19543,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
});
}
handleCallAccept(topicName, setCallTopic, callSeq) {
clearTimeout(this.callTimeoutTimer);
const topic = this.tinode.getTopic(topicName);
if (!topic) {
return;
@@ -19451,6 +19570,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}
switch (info.event) {
case 'accept':
clearTimeout(this.callTimeoutTimer);
if (tinode_sdk__WEBPACK_IMPORTED_MODULE_4__.Tinode.isMeTopicName(info.topic) && this.tinode.isMe(info.from)) {
this.setState({
callTopic: null,
@@ -20383,13 +20503,15 @@ function DeptNode({
const [children, setChildren] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
const [members, setMembers] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const load = () => {
if (children != null) return;
setLoading(true);
setLoadError(false);
Promise.all([(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getOrganizationDepartments)(dept.department_id).then(d => d.departments || d.items || []), dept.member_count ? (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getOrganizationDepartmentMembers)(dept.department_id).then(d => d.members || d.items || []) : Promise.resolve([])]).then(([depts, mems]) => {
setChildren(depts);
setMembers(mems);
}).finally(() => setLoading(false));
}).catch(() => setLoadError(true)).finally(() => setLoading(false));
};
const toggle = () => {
const next = !expanded;
@@ -20408,7 +20530,17 @@ function DeptNode({
}, void 0, false), expanded && loading ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("p", {
className: "organization-loading",
children: "加载中…"
}, void 0, false) : null, expanded && !loading ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.Fragment, {
}, void 0, false) : null, expanded && !loading && loadError ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("p", {
className: "organization-loading",
children: ["加载失败 ", (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("a", {
href: "#",
onClick: e => {
e.preventDefault();
load();
},
children: "重试"
}, void 0, false)]
}, void 0, true) : null, expanded && !loading && !loadError ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.Fragment, {
children: [(children || []).map(child => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(DeptNode, {
dept: child,
depth: depth + 1,
@@ -20921,13 +21053,21 @@ function OrganizationDirectory({
const [query, setQuery] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
const [searchResults, setSearchResults] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
const [roots, setRoots] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_4__.readCachedData)('org-roots') || null);
const [rootsError, setRootsError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [tenantName, setTenantName] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
const loadRoots = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
setRootsError(false);
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_3__.getOrganizationDepartments)('root').then(d => {
const list = d.departments || d.items || [];
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_4__.writeCachedData)('org-roots', list);
setRoots(list);
}).catch(() => setRoots(prev => prev || []));
}).catch(() => {
setRoots(prev => prev || []);
setRootsError(true);
});
}, []);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
loadRoots();
try {
const tenant = JSON.parse(window.localStorage.getItem('im-tenant') || '{}');
setTenantName(tenant.name || '');
@@ -20979,7 +21119,16 @@ function OrganizationDirectory({
children: "加载中…"
}, void 0, false) : roots.length === 0 ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("p", {
className: "organization-loading",
children: "暂无组织架构数据"
children: rootsError ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.Fragment, {
children: ["加载失败 ", (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("a", {
href: "#",
onClick: e => {
e.preventDefault();
loadRoots();
},
children: "重试"
}, void 0, false)]
}, void 0, true) : '暂无组织架构数据'
}, void 0, false) : roots.map(dept => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(DeptNode, {
dept: dept,
depth: 0,