第七轮深度测试:修复7个agent发现的12处前端问题,重点是列表分页截断与呼叫/播放状态机

- 通话状态机(Ch8):canStartCall 补齐通话中重复发起的防护,callShouldStart 补齐放弃/失败路径的清理
- 分页截断(Ch5-6):新增 fetchAllPages 辅助函数,修复通讯录/黑名单/好友申请/群成员/管理员/禁言等
  11 处列表只读首页导致真实成员被遗漏的问题;群角色判断改用不受分页影响的 getGroup().my_role
- 消息与媒体(Ch3-4/Ch7/Ch9):录音超时污染时长、手动重试丢失 head 导致消息被错误分类、批量删除
  未取消上传中附件、语音播放器补齐同时只能播一条、workplace-collaboration-view 冗余重试请求
- 独立密码/青少年模式(Ch10-11):设置密码后按调用来源判断是否应自动开启青少年模式,避免关闭流程
  中途"忘记密码"被误当成开启;注销账号确认文案补充群主后果说明

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-05 12:18:25 +08:00
co-authored by Claude Sonnet 5
parent f42b8536e5
commit bede45fd7f
19 changed files with 281 additions and 90 deletions
+19 -17
View File
@@ -10,7 +10,7 @@ import {
updateCurrentGroupAnnouncement, readGroupAnnouncement, getGroupGovernance, updateGroupGovernance, addGroupAdmins,
deleteGroupAdmin, muteGroupMembers, getGroupMutes, unmuteGroupMember, transferGroupOwner,
getGroupFiles, addGroupFile, deleteGroupFile, uploadLargeFile, getFileUrl, getUserProfile,
getGroupMemberProfile
getGroupMemberProfile, fetchAllPages
} from '../lib/im-api.js';
import { readCachedData, writeCachedData, useDebouncedValue } from '../lib/use-cached-data.js';
@@ -706,8 +706,9 @@ function AdminList({topic, onToast}) {
// Fetch the unfiltered member list rather than {role:'admin'} + a separate per-user profile lookup
// for the owner -- GET /users/{id}/profile 400s ("malformed request", field "user_id") when {id} is
// the caller's own uid, which broke this whenever the viewer is themselves the group's owner.
getGroupMembers(topic).then(data => {
const items = data.items || [];
// Members cap out at 30/page against a 128-member group limit -- an admin sitting past page 1
// used to vanish from this list entirely rather than the list just being incomplete.
fetchAllPages(cursor => getGroupMembers(topic, {cursor})).then(items => {
writeCachedData('group-members:' + topic, items);
const d = deriveOwnerAndAdmins(items);
setOwner(d.owner);
@@ -744,8 +745,7 @@ function GroupMembers({topic, onToast}) {
const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => {
if (!topic) return;
getGroupMembers(topic).then(data => {
const items = data.items || [];
fetchAllPages(cursor => getGroupMembers(topic, {cursor})).then(items => {
writeCachedData('group-members:' + topic, items);
setMembers(items);
}).catch(() => { onToast('加载失败'); setLoadError(true); });
@@ -781,8 +781,8 @@ function MemberPicker({type, topic, onToast}) {
const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => {
if (!topic) return;
getGroupMembers(topic, mute ? {muteEligible: true} : {eligibleRole: 'admin'})
.then(data => setMembers((data.items || []).filter(m => m.eligible !== false)))
fetchAllPages(cursor => getGroupMembers(topic, {cursor, ...(mute ? {muteEligible: true} : {eligibleRole: 'admin'})}))
.then(items => setMembers(items.filter(m => m.eligible !== false)))
.catch(() => { onToast('加载失败'); setLoadError(true); });
}, [topic, mute]);
const visible = (members || []).filter(m => (m.group_nickname || m.display_name || '').includes(query));
@@ -842,15 +842,14 @@ function Muted({topic, onToast}) {
const [loadError, setLoadError] = React.useState(false);
const load = React.useCallback(() => {
if (!topic) return;
getGroupMutes(topic).then(data => {
const list = data.items || [];
fetchAllPages(cursor => getGroupMutes(topic, cursor)).then(list => {
writeCachedData('group-muted:' + topic, list);
setItems(list);
}).catch(() => { onToast('加载失败'); setLoadError(true); });
getGroupMembers(topic).then(data => {
fetchAllPages(cursor => getGroupMembers(topic, {cursor})).then(items => {
const map = {};
const avatarMap = {};
(data.items || []).forEach(m => {
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};
});
@@ -896,7 +895,8 @@ function OwnerSelect({topic, onToast}) {
const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => {
if (!topic) return;
getGroupMembers(topic, {eligibleRole: 'owner'}).then(data => setMembers((data.items || []).filter(m => m.eligible !== false)))
fetchAllPages(cursor => getGroupMembers(topic, {cursor, eligibleRole: 'owner'}))
.then(items => setMembers(items.filter(m => m.eligible !== false)))
.catch(() => { onToast('加载失败'); setLoadError(true); });
}, [topic]);
const visible = (members || []).filter(m => (m.group_nickname || m.display_name || '').includes(query));
@@ -1122,11 +1122,13 @@ export default function GroupManagementView({view, hashParams, myUserId, tinode,
if (!topic || !myUserId) { setMyRole(null); return; }
const key = `group-my-role:${topic}:${myUserId}`;
setMyRole(readCachedData(key) || null);
getGroupMembers(topic).then(data => {
const items = data.items || [];
writeCachedData('group-members:' + topic, items);
const me = items.find(m => m.user_id === myUserId);
const role = me ? me.role : null;
// Used to look this up by paginating the full member list and finding myUserId in it (30/page
// against a 128-member cap) -- a real owner/admin sitting past page 1 read back as `null`,
// silently hiding every governance affordance gated on this value. getGroup's own `my_role`
// (already relied on elsewhere, e.g. the disband-group guard above) isn't paginated at all --
// it's always exactly "me", so this no longer needs the member list at all.
getGroup(topic).then(group => {
const role = group ? group.my_role : null;
writeCachedData(key, role);
setMyRole(role);
}).catch(() => {});