修复全页面走查发现的问题(第二批:登录、通讯录、群聊)

第1章
- tinode-web: 手输密码登录失败会 navigateTo('') 把用户弹回企业码页,刚输的账号丢了,而
  TenantEntry 不渲染 FormAlert,那句错误提示根本看不到。改成只有存储的 token 失效才回登录入口。
- auth-onboarding IdentityCheck: 限流 429 时服务端会连同未过期的 challenge_id 一起返回,
  原来只显示错误文案、challengeId 留空,于是 submit 开头的守卫静默吞掉点击——输完验证码点
  "确定"毫无反应也无提示。同文件 VerificationButton 早就处理对了,这里是漏抄。
- auth-onboarding Register: 手机号注册路径硬传 invite_code:'',把用户填的邀请码丢了。后端
  user.go:175-181 对邀请码的校验和 RecordUse 不区分注册路径,UI 上也摆着这个输入框。

第5章
- 好友列表主副行显示同一串字:后端在有备注时把 display_name 也设成备注(im_http.go:8500-8507),
  两个字段同值。加"不同才显示"的守卫。
- 拉黑确认弹窗把设置页当背景渲染并摆出"操作之后"的样子,但背景组件自己的 getUserProfile
  回来后又把开关改回未拉黑、"删除好友"红条重新冒出来。加 pinBlocked 挡住这次覆盖。

第6章
- 群列表把群名显示成 topic id、成员数恒为 0。根因在后端:handler 用 store.Users.GetSubs,而
  store 自己的注释就写着它 "Does not load Public/Trusted or Private"(store.go:834-835),
  groupListItemPayload 却去读 sub.GetPublic()/GetSubCnt()(应该用 GetTopics)。已记为后端问题;
  前端先用本地 Tinode 会话缓存兜底(消息首页一直这么取,是准的)。
- 禁言页每行头像画成群组图标(Avatar 兜底图标默认 'group',没传 icon="person"),
  且顺带从成员表带上真实头像。
- 禁言页"以上成员不允许发言"被当成空状态用,语义正好反了——有禁言成员时这句消失、
  一个都没有时反而显示。改为列表下方常驻注脚,空态另给文案,并补 .group-inline-note 样式。
- bytesToGB 只输出到 MB,1.4GB 的文件显示成 "1433.6 MB"。
- 群文件"来自"直接显示裸 uploader_id(groupFilePayload 不给昵称),拉成员表交叉引用。
- 群公告从聊天页横幅进入后返回,会把会话一起关掉:navigate 只写 query 参数,path[1] 仍为空,
  handleHashRoute 就把 topicSelected 清成 null。Header 支持 onBack,显式把 topic 写回 path[1]。
- 群成员点头像打不开非好友资料页:目标页先读 'profile-seed:' 缓存,因为 getUserProfile 对
  非好友直接 403。第5章入口一直有 seed,这里漏了。
- 群简介上限 225 超过后端硬限 200(im_http.go:4667),写满必被打回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-03 18:40:53 +08:00
co-authored by Claude Opus 5
parent 5a07bb4e3b
commit 02b98cceae
7 changed files with 224 additions and 42 deletions
+91 -19
View File
@@ -6155,7 +6155,7 @@ function Register({
onImRegisterRequest(secretAccount, form.password, form.username || '', {
username: form.username || '',
phone: form.phone,
invite_code: ''
invite_code: form.invite || ''
}, {
meth: 'tel',
val: form.phone,
@@ -6393,6 +6393,15 @@ function IdentityCheck({
setSeconds(data.resend_after_seconds || 60);
} catch (err) {
setError(err.reason === 'REALNAME_REQUIRED' ? '需要先完成实名认证并绑定手机号' : err.message || '获取验证码失败');
if (err.details && err.details.retry_after_seconds) {
setSeconds(err.details.retry_after_seconds);
}
if (err.details && err.details.challenge_id) {
setChallengeId(err.details.challenge_id);
}
if (err.details && err.details.phone_mask) {
setPhoneMask(err.details.phone_mask);
}
} finally {
setBusy(false);
}
@@ -7614,7 +7623,7 @@ function AddressBook({
}, 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: person.remark || person.display_name
}, void 0, false), person.remark ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
}, void 0, false), person.remark && person.remark !== person.display_name ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
children: person.display_name
}, void 0, false) : null]
}, void 0, true)]
@@ -8111,6 +8120,7 @@ function Toggle({
function Settings({
uid,
initialBlocked,
pinBlocked,
onToast
}) {
const [profile, setProfile] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => uid ? (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('profile-seed:' + uid) : null);
@@ -8125,7 +8135,7 @@ function Settings({
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getUserProfile)(uid).then(p => {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + uid, p);
setProfile(p);
setBlocked(p.relationship === 'blocked_by_me');
if (!pinBlocked) setBlocked(p.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);
@@ -8358,6 +8368,7 @@ function ConfirmSheet({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Settings, {
uid: uid,
initialBlocked: type === 'block',
pinBlocked: true,
onToast: onToast
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
className: "people-sheet-backdrop",
@@ -10319,7 +10330,25 @@ const navigate = (view, topic) => {
hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].removeUrlParam(hash, 'topic');
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
};
const navigateToProfile = userId => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
const navigateToProfile = (userId, member) => {
if (member && userId) {
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + userId, {
user_id: userId,
display_name: member.group_nickname || member.display_name || userId,
avatar_file_id: member.avatar_file_id,
...(member.signature ? {
signature: member.signature
} : {}),
...(member.region ? {
region: member.region
} : {}),
...(member.gender ? {
gender: member.gender
} : {})
});
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
};
const GROUP_MANAGEMENT_VIEWS = ['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'];
function alphaInitial(name) {
const ch = (name || '').trim()[0] || '#';
@@ -10343,6 +10372,18 @@ function groupBy(list, keyFn) {
});
return Object.keys(groups).sort().map(key => [key, groups[key]]);
}
function groupDisplayName(tinode, group) {
if (group.name && group.name !== group.topic) return group.name;
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
const pub = t && t.public;
const local = pub && (pub.name || pub.display_name || pub.fn);
return local || group.name || group.topic;
}
function groupMemberCount(tinode, group) {
if (group.member_count) return group.member_count;
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
return t && t.subcnt || 0;
}
function shortDate(iso) {
if (!iso) return '';
const d = new Date(iso);
@@ -10395,6 +10436,7 @@ function Header({
title,
back = 'group-list',
backTopic,
onBack,
right,
onRight
}) {
@@ -10403,7 +10445,9 @@ function Header({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
type: "button",
onClick: () => {
if (!_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].goBack()) navigate(back, backTopic);
if (!_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].goBack()) {
if (onBack) onBack();else navigate(back, backTopic);
}
},
"aria-label": "返回",
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
@@ -10514,6 +10558,7 @@ function Toggle({
}, void 0, false);
}
function GroupList({
tinode,
onToast
}) {
const [lists, setLists] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('groups') || {
@@ -10602,9 +10647,9 @@ function GroupList({
item: group
}, 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: group.name
children: groupDisplayName(tinode, group)
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
children: [group.member_count, " 位成员"]
children: [groupMemberCount(tinode, group), " 位成员"]
}, void 0, true)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
children: "chevron_right"
@@ -10919,7 +10964,9 @@ function Announcement({
const [saving, setSaving] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const canEdit = myRole === 'owner' || myRole === 'admin';
const backView = fromChat ? '' : 'group-settings';
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]);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!topic) return;
const cached = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-announcement:' + topic);
@@ -10950,8 +10997,9 @@ function Announcement({
className: "group-page",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Header, {
title: "群公告",
back: backView,
backTopic: topic
back: fromChat ? undefined : 'group-settings',
backTopic: topic,
onBack: fromChat ? backToChat : undefined
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
className: "group-announcement-detail",
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
@@ -10964,8 +11012,9 @@ function Announcement({
className: "group-page",
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Header, {
title: "群公告",
back: backView,
back: fromChat ? undefined : 'group-settings',
backTopic: topic,
onBack: fromChat ? backToChat : undefined,
right: canEdit ? '发布' : null,
onRight: canEdit ? publish : undefined
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
@@ -11106,7 +11155,7 @@ function GroupSettings({
className: "group-member-grid",
children: [members.slice(0, 9).map(item => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
type: "button",
onClick: () => navigateToProfile(item.user_id),
onClick: () => navigateToProfile(item.user_id, item),
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Avatar, {
item: item,
icon: "person"
@@ -11793,6 +11842,7 @@ function Muted({
}) {
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => topic && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-muted:' + topic) || null);
const [names, setNames] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
const [avatars, setAvatars] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const load = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
if (!topic) return;
@@ -11806,10 +11856,15 @@ function Muted({
});
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
const map = {};
const avatarMap = {};
(data.items || []).forEach(m => {
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
if (m.avatar_file_id) avatarMap[m.user_id] = {
avatar_file_id: m.avatar_file_id
};
});
setNames(map);
setAvatars(avatarMap);
}).catch(() => {});
}, [topic]);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
@@ -11832,12 +11887,13 @@ function Muted({
onRight: () => navigate('group-mute-add', topic)
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
className: "group-muted-list",
children: items === null ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
children: [items === null ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
className: "group-inline-empty",
children: loadError ? '成员列表加载失败' : '加载中…'
}, void 0, false) : items.length ? items.map(item => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("article", {
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Avatar, {
item: {}
item: avatars[item.user_id] || {},
icon: "person"
}, 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: names[item.user_id] || item.user_id
@@ -11851,9 +11907,12 @@ function Muted({
}, void 0, false)]
}, item.user_id, true)) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
className: "group-inline-empty",
children: "暂无禁言成员"
}, void 0, false), items && items.length ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
className: "group-inline-note",
children: "以上成员不允许发言"
}, void 0, false)
}, void 0, false)]
}, void 0, false) : null]
}, void 0, true)]
}, void 0, true);
}
function OwnerSelect({
@@ -11965,6 +12024,7 @@ function GroupFiles({
const [menuOpen, setMenuOpen] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [uploading, setUploading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [uploaderNames, setUploaderNames] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
const inputRefs = {
file: react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null),
image: react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null),
@@ -11986,6 +12046,16 @@ function GroupFiles({
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
load();
}, [load]);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
if (!topic) return;
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
const map = {};
(data.items || []).forEach(m => {
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
});
setUploaderNames(map);
}).catch(() => {});
}, [topic]);
const usedBytes = (files || []).reduce((sum, f) => sum + (f.file_size || 0), 0);
const pick = kind => {
setMenuOpen(false);
@@ -12122,7 +12192,7 @@ function GroupFiles({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
children: file.file_name || '未命名文件'
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
children: [bytesToGB(file.file_size), " · ", file.uploader_id]
children: ["来自 ", uploaderNames[file.uploader_id] || file.uploader_id, " · ", bytesToGB(file.file_size)]
}, void 0, true)]
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
type: "button",
@@ -12139,7 +12209,8 @@ function GroupFiles({
function bytesToGB(bytes) {
if (!bytes) return '0 KB';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
function GroupNameEditor({
topic,
@@ -12292,6 +12363,7 @@ function GroupManagementView({
}, [topic, myUserId]);
let content;
if (view === 'group-list' || view === 'group-empty') content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(GroupList, {
tinode: tinode,
onToast: setToast
}, void 0, false);else if (['group-invite', 'group-events', 'group-new-request', 'group-announcement', 'group-summary', 'group-role-message'].includes(view)) content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(GroupChat, {
mode: view,
@@ -17774,8 +17846,8 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
if (!autoLogin) {
if (token) {
this.handleLogout();
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo('');
}
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo('');
}
});
}
+1 -1
View File
File diff suppressed because one or more lines are too long