Update VPN proxy client behavior
This commit is contained in:
@@ -119,6 +119,9 @@ interface ClientOverviewPageProps {
|
||||
}
|
||||
|
||||
type CopyKind = 'gateway' | 'socks5' | 'http';
|
||||
type CopyFeedback = { failed: boolean; cycle: number };
|
||||
type CopyFeedbackMap = Partial<Record<CopyKind, CopyFeedback>>;
|
||||
type CopyAnnouncement = { text: string; cycle: number };
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
@@ -263,8 +266,9 @@ function InlineProgress({ operations, context }: {
|
||||
);
|
||||
}
|
||||
|
||||
function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSetGatewayAuto }: {
|
||||
function HarborBrand({ isGateway, connected, gatewayAvailable, gatewayDirect, blocked, onSetGatewayAuto }: {
|
||||
isGateway: boolean;
|
||||
connected: boolean;
|
||||
gatewayAvailable: boolean;
|
||||
gatewayDirect: boolean;
|
||||
blocked: boolean;
|
||||
@@ -333,7 +337,7 @@ function HarborBrand({ isGateway, gatewayAvailable, gatewayDirect, blocked, onSe
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<div className={`harbor-brand is-${product.toLowerCase()}${switchable ? ` is-switchable${gatewayDirect ? ' is-gateway-active' : ''}` : ''}`}>
|
||||
<div className={`harbor-brand is-${product.toLowerCase()}${connected ? ' is-connected' : ''}${switchable ? ` is-switchable${gatewayDirect ? ' is-gateway-active' : ''}` : ''}`}>
|
||||
{switchable ? <button
|
||||
className={`harbor-brand-control${modeAnimating ? ' is-mode-animating' : ''}`}
|
||||
type="button"
|
||||
@@ -392,8 +396,10 @@ export function ClientOverviewPage({
|
||||
const showPower = isGateway || (hasSubscription && Boolean(selectedServerId));
|
||||
const [now, setNow] = useState(Date.now());
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
const [copyFeedback, setCopyFeedback] = useState<{ kind: CopyKind; failed: boolean } | null>(null);
|
||||
const copyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [copyFeedback, setCopyFeedback] = useState<CopyFeedbackMap>({});
|
||||
const [copyAnnouncement, setCopyAnnouncement] = useState<CopyAnnouncement>({ text: '', cycle: 0 });
|
||||
const copyTimersRef = useRef<Partial<Record<CopyKind, ReturnType<typeof setTimeout>>>>({});
|
||||
const copyAttemptsRef = useRef<Partial<Record<CopyKind, object>>>({});
|
||||
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
|
||||
const controlHost = window.location.host || `${gatewayAddress}:3456`;
|
||||
const proxyUrls = localProxyUrls(state?.clientRuntime?.proxyPort, gatewayAddress);
|
||||
@@ -472,7 +478,8 @@ export function ClientOverviewPage({
|
||||
}, [diagnosticsAvailable]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
||||
for (const timer of Object.values(copyTimersRef.current)) clearTimeout(timer);
|
||||
copyAttemptsRef.current = {};
|
||||
}, []);
|
||||
|
||||
function selectServer(serverId: string) {
|
||||
@@ -482,14 +489,36 @@ export function ClientOverviewPage({
|
||||
|
||||
async function copyProxy(kind: CopyKind) {
|
||||
const value = kind === 'gateway' ? gatewayAddress : proxyUrls[kind];
|
||||
if (copyTimerRef.current) clearTimeout(copyTimerRef.current);
|
||||
const activeTimer = copyTimersRef.current[kind];
|
||||
if (activeTimer) clearTimeout(activeTimer);
|
||||
const attempt = {};
|
||||
copyAttemptsRef.current[kind] = attempt;
|
||||
let failed = false;
|
||||
try {
|
||||
await copyText(value);
|
||||
setCopyFeedback({ kind, failed: false });
|
||||
} catch {
|
||||
setCopyFeedback({ kind, failed: true });
|
||||
failed = true;
|
||||
}
|
||||
copyTimerRef.current = setTimeout(() => setCopyFeedback(null), 800);
|
||||
if (copyAttemptsRef.current[kind] !== attempt) return;
|
||||
const pendingTimer = copyTimersRef.current[kind];
|
||||
if (pendingTimer) clearTimeout(pendingTimer);
|
||||
setCopyFeedback((current) => ({
|
||||
...current,
|
||||
[kind]: { failed, cycle: (current[kind]?.cycle || 0) + 1 },
|
||||
}));
|
||||
setCopyAnnouncement((current) => ({
|
||||
text: failed ? 'Не удалось скопировать' : 'Скопировано',
|
||||
cycle: current.cycle + 1,
|
||||
}));
|
||||
copyTimersRef.current[kind] = setTimeout(() => {
|
||||
setCopyFeedback((current) => {
|
||||
const next = { ...current };
|
||||
delete next[kind];
|
||||
return next;
|
||||
});
|
||||
delete copyTimersRef.current[kind];
|
||||
delete copyAttemptsRef.current[kind];
|
||||
}, 800);
|
||||
}
|
||||
|
||||
function openRouting() {
|
||||
@@ -506,10 +535,11 @@ export function ClientOverviewPage({
|
||||
>
|
||||
<VersionDisplay isGateway={isGateway} versionInfo={versionInfo} />
|
||||
<div className="client-live-region" role="status" aria-live="polite" aria-atomic="true">
|
||||
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
|
||||
<span key={copyAnnouncement.cycle}>{copyAnnouncement.text}</span>
|
||||
</div>
|
||||
<HarborBrand
|
||||
isGateway={isGateway}
|
||||
connected={connected}
|
||||
gatewayAvailable={gatewayAvailable}
|
||||
gatewayDirect={gatewayDirect}
|
||||
blocked={gatewayAutoBlocked}
|
||||
|
||||
Reference in New Issue
Block a user