按产品决定移除面向国内客户不适用的自主注册入口:删除 /register 路由 及 RegisterPage、登录页"马上注册"链接、登录/注册共用的协议勾选组件 Agreement,以及仅注册流程使用的 registerAccount/queueAccExtra 等 死代码;登录不再依赖勾选协议才能提交。 修复转发消息报错(真实高频bug,不限于个别场景):`forwardMessage`/ `sendText` 发送前只是 `tinode.getTopic()` 取一个本地话题对象,从未 真正订阅——SDK 要求话题处于 attached 状态才允许发布,而只有当前正 打开的会话才满足这一点,一旦切换过/离开过某个会话(哪怕之前订阅过 预览),它就会变回未订阅状态。也就是说转发给"当前会话之外"几乎任何 一个联系人都会失败。新增 `ensureTopicAttached()`,在转发前按需临时 订阅目标话题,单条转发和多选批量转发两处都已接入,实机验证对未打开 过的好友/群转发均成功。 转发选择器(ForwardPickerModal)按好友/群聊分组显示(原来混在一个 列表里),并加了"全选/取消全选",避免多个接收方时逐条点选。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
import { requestVerificationCode } from '@/lib/api/auth';
|
|
import { ImApiError } from '@/lib/api/client';
|
|
|
|
const PHONE_RE = /^\d{11}$/;
|
|
|
|
interface VerificationCodeButtonProps {
|
|
phone: string;
|
|
scene: 'reset_password';
|
|
onChallenge: (challengeId: string) => void;
|
|
onError: (message: string) => void;
|
|
}
|
|
|
|
/** "获取验证码" button with resend countdown. Ported from the old app's VerificationButton. */
|
|
export function VerificationCodeButton({ phone, scene, onChallenge, onError }: VerificationCodeButtonProps) {
|
|
const [seconds, setSeconds] = useState(0);
|
|
const [busy, setBusy] = useState(false);
|
|
const timerRef = useRef<number | undefined>(undefined);
|
|
|
|
// Without this, navigating away mid-countdown (e.g. switching from register back to login)
|
|
// leaves the interval running against an unmounted component indefinitely until it naturally
|
|
// reaches 0 -- a real resource leak, not just a React warning, since nothing else ever clears it.
|
|
useEffect(() => () => window.clearInterval(timerRef.current), []);
|
|
|
|
// A countdown started for one phone number must not block sending a code to a DIFFERENT one --
|
|
// otherwise correcting a mistyped number mid-cooldown leaves the button stuck disabled (still
|
|
// counting down for the old number) until the old timer happens to run out on its own.
|
|
useEffect(() => {
|
|
window.clearInterval(timerRef.current);
|
|
setSeconds(0);
|
|
}, [phone]);
|
|
|
|
const tick = (initial: number) => {
|
|
window.clearInterval(timerRef.current);
|
|
setSeconds(initial);
|
|
timerRef.current = window.setInterval(() => {
|
|
setSeconds((s) => {
|
|
if (s <= 1) {
|
|
window.clearInterval(timerRef.current);
|
|
return 0;
|
|
}
|
|
return s - 1;
|
|
});
|
|
}, 1000);
|
|
};
|
|
|
|
const click = async () => {
|
|
if (busy || seconds > 0 || !PHONE_RE.test(phone)) return;
|
|
setBusy(true);
|
|
try {
|
|
const data = await requestVerificationCode(scene, phone);
|
|
onChallenge(data.challenge_id);
|
|
tick(data.resend_after_seconds || 60);
|
|
} catch (err) {
|
|
onError(err instanceof ImApiError ? err.message : '获取验证码失败');
|
|
// Rate-limited but a still-valid challenge already exists for this phone/scene -- reuse it.
|
|
const details = err instanceof ImApiError ? (err.details as Record<string, unknown> | undefined) : undefined;
|
|
if (typeof details?.retry_after_seconds === 'number') tick(details.retry_after_seconds);
|
|
if (typeof details?.challenge_id === 'string') onChallenge(details.challenge_id);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={click}
|
|
disabled={busy || seconds > 0 || !PHONE_RE.test(phone)}
|
|
className="h-9 w-32 flex-none rounded-lg bg-accent-soft-bg text-sm text-accent disabled:bg-[#e6e9ee] disabled:text-text-tertiary-3"
|
|
>
|
|
{seconds > 0 ? `重新获取 (${seconds}s)` : busy ? '发送中…' : '获取验证码'}
|
|
</button>
|
|
);
|
|
}
|