fix and update
This commit is contained in:
@@ -76,9 +76,10 @@ function Row({
|
||||
|
||||
export default function DataPage() {
|
||||
const router = useRouter();
|
||||
const { user, token, refreshProfile } = useAuth();
|
||||
const { user, token, applyUserPatch } = useAuth();
|
||||
const { isReady, isPinLocked } = useRequireAuth();
|
||||
const { showToast } = useToast();
|
||||
const userId = user?.id;
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [backupEmail, setBackupEmail] = useState('');
|
||||
@@ -98,15 +99,15 @@ export default function DataPage() {
|
||||
const [cancellingDeletion, setCancellingDeletion] = useState(false);
|
||||
|
||||
const loadDocuments = useCallback(async () => {
|
||||
if (!user || !token || isPinLocked) return;
|
||||
if (!userId || !token || isPinLocked) return;
|
||||
try {
|
||||
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${user.id}`, {}, token);
|
||||
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${userId}`, {}, token);
|
||||
setDocuments(response.documents ?? []);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, 'Не удалось загрузить документы');
|
||||
if (message) showToast(message);
|
||||
}
|
||||
}, [isPinLocked, showToast, token, user]);
|
||||
}, [isPinLocked, showToast, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
@@ -124,31 +125,31 @@ export default function DataPage() {
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !user || !token || isPinLocked) return;
|
||||
void apiFetch<UserProfileResponse>(`/profile/users/${user.id}`, {}, token)
|
||||
if (!isReady || !userId || !token || isPinLocked) return;
|
||||
void apiFetch<UserProfileResponse>(`/profile/users/${userId}`, {}, token)
|
||||
.then((profile) => {
|
||||
if (profile.birthDate) setBirthDate(profile.birthDate);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, [isPinLocked, isReady, token, user]);
|
||||
}, [isPinLocked, isReady, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && user && !isPinLocked) void loadDocuments();
|
||||
}, [isPinLocked, isReady, loadDocuments, user]);
|
||||
if (isReady && userId && !isPinLocked) void loadDocuments();
|
||||
}, [isPinLocked, isReady, loadDocuments, userId]);
|
||||
|
||||
const loadDeletionStatus = useCallback(async () => {
|
||||
if (!user || !token || isPinLocked) return;
|
||||
if (!userId || !token || isPinLocked) return;
|
||||
try {
|
||||
const status = await fetchAccountDeletionStatus(user.id, token);
|
||||
const status = await fetchAccountDeletionStatus(userId, token);
|
||||
setDeletionStatus(status);
|
||||
} catch {
|
||||
setDeletionStatus(null);
|
||||
}
|
||||
}, [isPinLocked, token, user]);
|
||||
}, [isPinLocked, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && user && !isPinLocked) void loadDeletionStatus();
|
||||
}, [isPinLocked, isReady, loadDeletionStatus, user]);
|
||||
if (isReady && userId && !isPinLocked) void loadDeletionStatus();
|
||||
}, [isPinLocked, isReady, loadDeletionStatus, userId]);
|
||||
|
||||
const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]);
|
||||
|
||||
@@ -186,7 +187,13 @@ export default function DataPage() {
|
||||
}, token);
|
||||
}
|
||||
|
||||
await refreshProfile();
|
||||
applyUserPatch({
|
||||
displayName: trimmedName || user.displayName,
|
||||
email: contacts.email ?? user.email,
|
||||
phone: contacts.phone ?? user.phone,
|
||||
backupEmail: contacts.backupEmail ?? user.backupEmail,
|
||||
backupPhone: contacts.backupPhone ?? user.backupPhone
|
||||
});
|
||||
showToast('Профиль обновлён');
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, 'Не удалось обновить профиль');
|
||||
@@ -244,7 +251,9 @@ export default function DataPage() {
|
||||
verificationIcon={user.verificationIcon}
|
||||
className="h-14 w-14"
|
||||
badgeSize="sm"
|
||||
onUpdated={refreshProfile}
|
||||
onUpdated={async () => {
|
||||
applyUserPatch({ hasAvatar: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AvatarDisplay userId="" displayName="" hasAvatar={false} token={null} className="h-14 w-14" />
|
||||
|
||||
@@ -40,7 +40,7 @@ export function FamilySidebarPanel() {
|
||||
}
|
||||
fetchChatRooms(group.id, token)
|
||||
.then((response) => setRooms(response.rooms ?? []))
|
||||
.catch(() => setRooms([]));
|
||||
.catch(() => undefined);
|
||||
}, [group?.id, token]);
|
||||
|
||||
const roomByPeerUserId = useMemo(() => {
|
||||
|
||||
@@ -56,6 +56,7 @@ interface AuthContextValue {
|
||||
verifyTotpLogin: (totpChallengeToken: string, code: string) => Promise<AuthTokens>;
|
||||
completePin: (sessionId: string, pin: string) => Promise<void>;
|
||||
applyLoginAuth: (auth: AuthTokens) => Promise<AuthTokens>;
|
||||
applyUserPatch: (patch: Partial<PublicUser>) => void;
|
||||
register: (data: { displayName: string; login: string; password: string }) => Promise<void>;
|
||||
refreshProfile: () => Promise<void>;
|
||||
logout: () => void;
|
||||
@@ -273,6 +274,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
router.push('/auth/login');
|
||||
}, [clearPinLock, router]);
|
||||
|
||||
const applyUserPatch = React.useCallback((patch: Partial<PublicUser>) => {
|
||||
setUser((current) => {
|
||||
if (!current) return current;
|
||||
const next = { ...current, ...patch };
|
||||
cacheUserProfile(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refreshProfile = React.useCallback(async () => {
|
||||
if (refreshInFlightRef.current) {
|
||||
return refreshInFlightRef.current;
|
||||
@@ -691,6 +701,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
verifyTotpLogin,
|
||||
completePin,
|
||||
applyLoginAuth,
|
||||
applyUserPatch,
|
||||
register,
|
||||
refreshProfile,
|
||||
logout
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFamilyOverlay } from '@/components/family/family-overlay-provider';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import {
|
||||
@@ -15,23 +15,26 @@ import {
|
||||
export function useSelectedFamily(enabled = true) {
|
||||
const { user, token, isPinLocked, isLoading, isContentReady } = useAuth();
|
||||
const { selectedGroupId, setSelectedGroupId } = useFamilyOverlay();
|
||||
const userId = user?.id;
|
||||
const [groups, setGroups] = useState<FamilyGroup[]>([]);
|
||||
const [group, setGroup] = useState<FamilyGroup | null>(null);
|
||||
const [presenceMembers, setPresenceMembers] = useState<FamilyPresenceMember[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const hasLoadedRef = useRef(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const accessToken = getAccessToken() ?? token?.trim() ?? null;
|
||||
if (!enabled || !user || !accessToken || isPinLocked || isLoading || !isContentReady) {
|
||||
if (!enabled || !userId || !accessToken || isPinLocked || isLoading || !isContentReady) {
|
||||
setGroups([]);
|
||||
setGroup(null);
|
||||
setPresenceMembers([]);
|
||||
hasLoadedRef.current = false;
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const groupsResponse = await fetchFamilyGroups(user.id, accessToken);
|
||||
const groupsResponse = await fetchFamilyGroups(userId, accessToken);
|
||||
const list = groupsResponse.groups ?? [];
|
||||
setGroups(list);
|
||||
|
||||
@@ -39,6 +42,7 @@ export function useSelectedFamily(enabled = true) {
|
||||
setGroup(null);
|
||||
setPresenceMembers([]);
|
||||
if (selectedGroupId) setSelectedGroupId(null);
|
||||
hasLoadedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,19 +59,26 @@ export function useSelectedFamily(enabled = true) {
|
||||
]);
|
||||
setGroup(groupResponse);
|
||||
setPresenceMembers(presenceResponse.members ?? []);
|
||||
hasLoadedRef.current = true;
|
||||
} catch {
|
||||
setGroups([]);
|
||||
setGroup(null);
|
||||
setPresenceMembers([]);
|
||||
// Фоновая ошибка не должна сбрасывать уже показанную семью в сайдбаре.
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [enabled, isContentReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, user]);
|
||||
}, [enabled, isContentReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
if (!enabled || !userId || !token || isPinLocked || isLoading || !isContentReady) {
|
||||
setGroups([]);
|
||||
setGroup(null);
|
||||
setPresenceMembers([]);
|
||||
hasLoadedRef.current = false;
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(!hasLoadedRef.current);
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
}, [enabled, isContentReady, isLoading, isPinLocked, refresh, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
const accessToken = getAccessToken() ?? token?.trim() ?? null;
|
||||
|
||||
@@ -677,7 +677,7 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
|
||||
}
|
||||
|
||||
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
||||
const GATEWAY_RETRY_ATTEMPTS = 4;
|
||||
const GATEWAY_RETRY_ATTEMPTS = 2;
|
||||
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
|
||||
const GATEWAY_STABLE_MAX_ROUNDS = 8;
|
||||
const API_MAX_CONCURRENT = 6;
|
||||
@@ -827,7 +827,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
|
||||
}
|
||||
|
||||
function gatewayRetryDelay(attempt: number): number {
|
||||
return 800 + attempt * 700;
|
||||
return 600 + attempt * 500;
|
||||
}
|
||||
|
||||
/** Повтор при кратковременном 502/503/504 без сброса всего UI в bootstrap. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
||||
const MEDIA_FETCH_MAX_ATTEMPTS = 8;
|
||||
const MEDIA_FETCH_MAX_ATTEMPTS = 3;
|
||||
const MEDIA_SLOW_LOAD_WARN_MS = 8_000;
|
||||
const MEDIA_STREAM_PATH = '/media/stream/';
|
||||
const SAME_ORIGIN_API_PREFIX = '/idp-api';
|
||||
|
||||
Reference in New Issue
Block a user