第三轮深度测试:修复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:
co-authored by
Claude Sonnet 5
parent
505a6d7d56
commit
93b856aec8
+184
-35
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user