第七轮深度测试:修复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
+35 -8
View File
@@ -14,7 +14,9 @@ const INVITE_RE = /^[A-Za-z]{5}$/;
const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const navigate = view => HashNavigation.navigateTo(view);
// params is optional and additive only -- every existing call site that passes just `view` keeps
// producing the exact same bare hash as the old `HashNavigation.navigateTo(view)` did.
const navigate = (view, params) => HashNavigation.navigateTo(HashNavigation.composeUrlHash([view], params || {}));
function Icon({children, className = ''}) {
return <i className={'material-icons ' + className} aria-hidden="true">{children}</i>;
@@ -529,7 +531,13 @@ function PasswordReset() {
// successful verify, stashes the resulting action_token for the next screen to spend.
const IDENTITY_CHECK_SCENE = 'set_independent_password';
function IdentityCheck({authenticated}) {
function IdentityCheck({authenticated, hashParams}) {
// Carries through whichever youth-mode flow (enable/disable) sent the user here via "忘记独立
// 密码?", if any -- IndependentPassword needs it to know whether to auto-continue into actually
// enabling youth mode, since the server hands back an enable-flow token unconditionally on every
// successful password (re)set regardless of why the caller got here. Absent (undefined) for the
// unrelated generic "身份验证" entry in acc-security-view.jsx, which should never touch youth mode.
const mode = hashParams && hashParams.ym;
const [phoneMask, setPhoneMask] = React.useState('');
const [challengeId, setChallengeId] = React.useState('');
const [code, setCode] = React.useState('');
@@ -581,7 +589,7 @@ function IdentityCheck({authenticated}) {
try {
const data = await verifySecurityChallenge(challengeId, code);
sessionStorage.setItem('im-action-token:' + IDENTITY_CHECK_SCENE, JSON.stringify(data));
navigate('independent-password');
navigate('independent-password', mode ? {ym: mode} : undefined);
} catch (err) {
setError(err.message || '验证码错误');
} finally {
@@ -681,7 +689,12 @@ function RealnameReminder() {
</div>;
}
function IndependentPassword({authenticated}) {
function IndependentPassword({authenticated, hashParams}) {
// Only the genuine "haven't set a password yet, setting one now to enable youth mode for the
// first time" flow should auto-continue into enableYouthMode below -- see IdentityCheck's
// comment on the same param for why this can't just be inferred from enable_action_token being
// present.
const mode = hashParams && hashParams.ym;
const [pin, setPin] = React.useState('');
const [error, setError] = React.useState('');
const [toast, setToast] = React.useState('');
@@ -704,8 +717,13 @@ function IndependentPassword({authenticated}) {
// 见 server/im_http.go:6178-6189)。原来这里把它整个丢掉、只弹一句"设置成功"就停在
// PIN 页——用户走完短信验证+设密码,青少年模式其实还是关的,得自己摸回设置页再输一遍。
// 现在拿着这个 token 把最后一步做完。
// enable_action_token comes back unconditionally on every successful (re)set, regardless of
// why the caller ended up here (server doesn't know or care) -- only the genuine "first time
// enabling" flow should spend it. Otherwise this silently flipped youth mode on for someone
// who was mid-way through disabling it, or who came from the unrelated generic "身份验证"
// entry with no youth-mode intent at all.
const enableToken = result && result.enable_action_token;
if (enableToken) {
if (enableToken && mode === 'enable') {
try {
const current = await getYouthMode();
await enableYouthMode(current.policy_version, enableToken);
@@ -719,6 +737,15 @@ function IndependentPassword({authenticated}) {
return;
}
}
if (mode === 'disable') {
// enableToken (if any) can't be spent here -- ConsumeActionToken requires an exact
// action='close_youth_mode' match, and this one is always tagged 'enable_youth_mode'. The
// user does know the fresh PIN now though, so send them straight back to actually finish
// disabling instead of leaving them stuck on this screen with nothing left to do.
setError(''); setToast('独立密码已重置,请使用新密码关闭青少年模式');
window.setTimeout(() => navigate('youth-mode-verify', {ym: 'disable'}), 800);
return;
}
setError(''); setToast('独立密码设置成功');
} catch (err) {
setError(err.message || '设置失败,请返回重新验证身份');
@@ -740,7 +767,7 @@ function IndependentPassword({authenticated}) {
}
export default function AuthOnboardingView({
view, onTenantResolved, authenticated = false,
view, hashParams, onTenantResolved, authenticated = false,
login, loginDisabled, persist, errorText, errorLevel,
onLoginRequest, onPersistenceChange, onImRegisterRequest, onError
}) {
@@ -753,11 +780,11 @@ export default function AuthOnboardingView({
case 'enterprise-feedback': return <EnterpriseFeedback />;
case 'register': return <Register errorText={errorText} errorLevel={errorLevel} onImRegisterRequest={onImRegisterRequest} />;
case 'reset': return <PasswordReset />;
case 'identity-check': return <IdentityCheck authenticated={authenticated} />;
case 'identity-check': return <IdentityCheck authenticated={authenticated} hashParams={hashParams} />;
case 'realname': return <Realname authenticated={authenticated} />;
case 'realname-verified': return <Realname authenticated={authenticated} />;
case 'realname-reminder': return <RealnameReminder />;
case 'independent-password': return <IndependentPassword authenticated={authenticated} />;
case 'independent-password': return <IndependentPassword authenticated={authenticated} hashParams={hashParams} />;
default: return <TenantEntry onTenantResolved={onTenantResolved || (() => {})} />;
}
}