fix and update

This commit is contained in:
lendry
2026-06-29 12:17:25 +03:00
parent 923a028cdd
commit 75ccbe5fc4
39 changed files with 1619 additions and 63 deletions

View File

@@ -10,7 +10,7 @@ interface ChatImageLightboxProps {
open: boolean;
images: ChatMessage[];
index: number;
mediaUrls: Record<string, { blobUrl: string; fileName?: string } | undefined>;
mediaUrls: Record<string, { blobUrl?: string; fileName?: string } | undefined>;
onClose: () => void;
onIndexChange: (index: number) => void;
onDownload?: (message: ChatMessage) => void;

View File

@@ -6,7 +6,7 @@ import { cn } from '@/lib/utils';
interface ChatMediaAlbumGridProps {
messages: ChatMessage[];
mediaUrls: Record<string, { blobUrl: string } | undefined>;
mediaUrls: Record<string, { blobUrl?: string } | undefined>;
className?: string;
onImageClick?: (message: ChatMessage, index: number) => void;
}

View File

@@ -275,7 +275,7 @@ export function ChatMediaComposeDialog({
type="file"
className="hidden"
multiple
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar,.7z,.txt"
accept="*/*"
onChange={(event) => {
const files = Array.from(event.target.files ?? []);
if (files.length) onAddFiles(files);

View File

@@ -152,10 +152,12 @@ interface PresignedUploadResponse {
}
interface LoadedChatMedia {
blobUrl: string;
expiresAt: number;
blobUrl?: string;
expiresAt?: number;
fileName?: string;
mimeType?: string;
status: 'loading' | 'ready' | 'error';
errorMessage?: string;
}
function formatMessageTime(value: string) {
@@ -318,6 +320,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const recordingStartedAtRef = useRef<number | null>(null);
const mediaUrlsRef = useRef<Record<string, LoadedChatMedia>>({});
const mediaLoadInFlightRef = useRef<Set<string>>(new Set());
const fileInputRef = useRef<HTMLInputElement>(null);
const dragDepthRef = useRef(0);
@@ -678,7 +681,37 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
if (!token || !activeRoomId || !message.storageKey) return;
const storageKey = message.storageKey;
const existing = mediaUrlsRef.current[storageKey];
if (!force && existing && !shouldRefreshMedia(existing.expiresAt)) return;
if (!force && existing?.status === 'ready' && existing.blobUrl && existing.expiresAt && !shouldRefreshMedia(existing.expiresAt)) {
return;
}
if (mediaLoadInFlightRef.current.has(storageKey) && !force) {
return;
}
if (message.isEncrypted && activeRoomIsE2E) {
if (peerKeyLoading) return;
if (!peerE2eKey || !user?.id || !message.content) {
setMediaUrls((current) => ({
...current,
[storageKey]: {
...current[storageKey],
status: 'error',
errorMessage: 'Не удалось расшифровать файл'
}
}));
return;
}
}
mediaLoadInFlightRef.current.add(storageKey);
setMediaUrls((current) => ({
...current,
[storageKey]: {
...current[storageKey],
status: 'loading',
errorMessage: undefined
}
}));
let meta = parseMessageMetadata(message.metadataJson);
let fileName = meta.fileName || message.content || undefined;
@@ -709,23 +742,50 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
blobUrl,
expiresAt: mediaExpiresAtMs(access.expiresAt),
fileName,
mimeType
mimeType,
status: 'ready'
}
};
});
} catch {
// ignore failed media loads in background
} catch (error) {
setMediaUrls((current) => ({
...current,
[storageKey]: {
...current[storageKey],
status: 'error',
errorMessage: error instanceof Error ? error.message : 'Не удалось загрузить файл'
}
}));
} finally {
mediaLoadInFlightRef.current.delete(storageKey);
}
},
[activeRoomId, activeRoomIsE2E, peerE2eKey, token, user?.id]
[activeRoomId, activeRoomIsE2E, peerE2eKey, peerKeyLoading, token, user?.id]
);
const cacheUploadedMedia = useCallback((storageKey: string, file: File) => {
const blobUrl = createBlobObjectUrl(file);
setMediaUrls((current) => {
revokeBlobObjectUrl(current[storageKey]?.blobUrl);
return {
...current,
[storageKey]: {
blobUrl,
expiresAt: Date.now() + 30 * 60_000,
fileName: file.name,
mimeType: file.type || resolveChatContentType(file),
status: 'ready'
}
};
});
}, []);
useEffect(() => {
if (!token || !activeRoomId) return;
messages.forEach((message) => {
if (message.storageKey) void loadChatMedia(message);
});
}, [activeRoomId, loadChatMedia, messages, token]);
}, [activeRoomId, loadChatMedia, messages, peerE2eKey, peerKeyLoading, token]);
useEffect(() => {
if (!token || !activeRoomId) return;
@@ -733,7 +793,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
messages.forEach((message) => {
if (!message.storageKey) return;
const cached = mediaUrlsRef.current[message.storageKey];
if (cached && shouldRefreshMedia(cached.expiresAt)) {
if (cached?.status === 'ready' && cached.expiresAt && shouldRefreshMedia(cached.expiresAt)) {
void loadChatMedia(message, true);
}
});
@@ -1323,6 +1383,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
token
);
appendMessage(message);
cacheUploadedMedia(presigned.storageKey, file);
void loadGroup();
} catch (error) {
showToast(getApiErrorMessage(error, 'Не удалось отправить файл') ?? 'Ошибка');
@@ -1366,7 +1427,13 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
peerE2eKey,
encryptOutgoing
});
uploaded.forEach((message) => appendMessage(message));
uploaded.forEach((message, index) => {
appendMessage(message);
const sourceFile = mediaComposer.items[index]?.file;
if (message.storageKey && sourceFile) {
cacheUploadedMedia(message.storageKey, sourceFile);
}
});
mediaComposer.close();
void loadGroup();
} catch (error) {
@@ -1972,7 +2039,21 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
const media = mediaUrls[message.storageKey];
const meta = parseMessageMetadata(message.metadataJson);
const fileName = e2ePayload?.fileName || meta.fileName || message.content || 'Файл';
if (!media?.blobUrl) {
if (media?.status === 'error') {
return (
<div className="space-y-2 text-sm text-[#667085]">
<p>{media.errorMessage ?? 'Не удалось загрузить файл'}</p>
<button
type="button"
className="font-medium text-[#3390ec] hover:underline"
onClick={() => void loadChatMedia(message, true)}
>
Повторить
</button>
</div>
);
}
if (!media?.blobUrl || media.status === 'loading') {
return (
<div className="flex items-center gap-2 text-sm text-[#667085]">
<Loader2 className="h-4 w-4 animate-spin" />
@@ -2275,7 +2356,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
type="file"
className="hidden"
multiple
accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar,.7z,.txt"
accept="*/*"
onChange={(event) => {
const files = Array.from(event.target.files ?? []);
if (files.length) queueMediaFiles(files);

View File

@@ -1386,7 +1386,7 @@ export function MiniFamilyChat() {
ref={fileInputRef}
type="file"
multiple
accept="image/*,audio/*,video/*,.pdf,.doc,.docx,.txt,.zip,.rar,.7z"
accept="*/*"
className="hidden"
onChange={(event) => {
const files = Array.from(event.target.files ?? []);

View File

@@ -11,6 +11,8 @@ import { BrandLogo } from '@/components/id/brand-logo';
import Link from 'next/link';
import { cn } from '@/lib/utils';
const MOBILE_HEADER_HEIGHT = 'calc(3.5rem + env(safe-area-inset-top))';
export function IdShell({
active,
children,
@@ -25,10 +27,13 @@ export function IdShell({
return (
<FamilyOverlayProvider>
<EmojiSpriteProvider />
<div className="min-h-screen overflow-x-hidden bg-white">
<div className="min-h-screen bg-white">
<Sidebar active={active} />
{!fullBleed ? (
<div className="fixed left-0 right-0 top-0 z-40 flex items-center justify-between border-b border-[#eceef4] bg-white/95 px-4 py-3 backdrop-blur lg:hidden">
<div
className="fixed left-0 right-0 top-0 z-40 flex h-[var(--mobile-header-height)] items-center justify-between border-b border-[#eceef4] bg-white/95 px-4 backdrop-blur lg:hidden"
style={{ ['--mobile-header-height' as string]: MOBILE_HEADER_HEIGHT, paddingTop: 'env(safe-area-inset-top)' }}
>
<Link href="/" className="shrink-0">
<BrandLogo size="sm" variant="dark" />
</Link>
@@ -44,9 +49,10 @@ export function IdShell({
</div>
<main
className={cn(
'pb-[calc(4.5rem+env(safe-area-inset-bottom))] lg:pb-8 lg:pl-[220px] lg:pr-8',
fullBleed ? 'px-0 pt-0 lg:pt-8' : 'px-0 pt-14 lg:pt-8'
'min-w-0 pb-[calc(4.5rem+env(safe-area-inset-bottom))] lg:pb-8 lg:pl-[220px] lg:pr-8',
fullBleed ? 'px-0 pt-0 lg:pt-8' : 'px-0 pt-[var(--mobile-header-height)] lg:pt-8'
)}
style={fullBleed ? undefined : ({ ['--mobile-header-height' as string]: MOBILE_HEADER_HEIGHT } as React.CSSProperties)}
>
<div
className={cn(

View File

@@ -2,7 +2,21 @@ import * as React from 'react';
import { cn } from '@/lib/utils';
export function Table({ className, ...props }: React.TableHTMLAttributes<HTMLTableElement>) {
return <table className={cn('w-full caption-bottom text-sm', className)} {...props} />;
return <table className={cn('w-full min-w-[720px] caption-bottom text-sm', className)} {...props} />;
}
export function TableContainer({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
'w-full max-w-full overflow-x-auto overscroll-x-contain [-webkit-overflow-scrolling:touch]',
className
)}
{...props}
>
{children}
</div>
);
}
export function TableHeader({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {