From a0966e7ba248de1526fa49629dbe15fad773aadd Mon Sep 17 00:00:00 2001 From: lendry Date: Fri, 10 Jul 2026 12:14:06 +0300 Subject: [PATCH] fix --- .../components/family/family-group-view.tsx | 21 +++++----- .../frontend/components/id/pin-lock-modal.tsx | 16 +++++++- apps/frontend/components/ui/dialog.tsx | 36 +++++++++++++++-- apps/frontend/lib/chat-media-upload.ts | 6 ++- apps/frontend/lib/chat-media.ts | 39 ++++++++++++++++++- apps/frontend/lib/chat-message-utils.ts | 11 ++++++ 6 files changed, 112 insertions(+), 17 deletions(-) diff --git a/apps/frontend/components/family/family-group-view.tsx b/apps/frontend/components/family/family-group-view.tsx index ea64f18..54c3d3c 100644 --- a/apps/frontend/components/family/family-group-view.tsx +++ b/apps/frontend/components/family/family-group-view.tsx @@ -148,6 +148,7 @@ import { detectChatMessageType, fileIconLabel, formatFileSize, + isVideoNoteMessage, parseLocationMetadata, parseMessageMetadata, resolveChatContentType @@ -2182,6 +2183,16 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { /> ); } + if (isVideoNoteMessage(message, e2ePayload)) { + const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs; + return ( + + ); + } if (message.type === 'VIDEO') { const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs; return ( @@ -2193,16 +2204,6 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { /> ); } - if (message.type === 'VIDEO_NOTE') { - const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs; - return ( - - ); - } return ( + {onLogout ? ( + + ) : null} diff --git a/apps/frontend/components/ui/dialog.tsx b/apps/frontend/components/ui/dialog.tsx index 6f76c86..747069b 100644 --- a/apps/frontend/components/ui/dialog.tsx +++ b/apps/frontend/components/ui/dialog.tsx @@ -7,11 +7,41 @@ import { cn } from '@/lib/utils'; export const Dialog = DialogPrimitive.Root; export const DialogTrigger = DialogPrimitive.Trigger; -export function DialogContent({ className, children, ...props }: React.ComponentProps) { +export function DialogContent({ + className, + placement = 'center', + children, + style, + ...props +}: React.ComponentProps & { + placement?: 'center' | 'upper'; +}) { + const placementStyle: React.CSSProperties = + placement === 'upper' + ? { + top: 'max(1.5rem, 10vh)', + left: '50%', + transform: 'translateX(-50%)', + margin: 0 + } + : { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + margin: 0 + }; + + const overlayZ = placement === 'upper' ? 'z-[300]' : 'z-[200]'; + const contentZ = placement === 'upper' ? 'z-[301]' : 'z-[201]'; + return ( - - + + {children} diff --git a/apps/frontend/lib/chat-media-upload.ts b/apps/frontend/lib/chat-media-upload.ts index bf7da4d..77d7f9b 100644 --- a/apps/frontend/lib/chat-media-upload.ts +++ b/apps/frontend/lib/chat-media-upload.ts @@ -6,7 +6,7 @@ import { type ChatMessage } from '@/lib/api'; import { compressChatImage, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress'; -import { detectChatMessageType, resolveChatContentType } from '@/lib/chat-media'; +import { detectChatMessageType, isVideoNoteFileName, resolveChatContentType } from '@/lib/chat-media'; import { e2eKindFromMessageType, encryptE2EBlob, type E2EMessagePayload } from '@/lib/e2e-chat'; export interface ChatMediaUploadItem { @@ -54,6 +54,7 @@ export async function uploadChatMediaBatch(params: { const contentType = resolveChatContentType(file); const messageType = item.sendAsFile && isCompressibleImageFile(item.file) ? 'FILE' : detectChatMessageType(file); + const videoNote = messageType === 'VIDEO_NOTE' || isVideoNoteFileName(item.file.name); const metadata = { fileName: item.file.name, fileSize: file.size, @@ -62,7 +63,8 @@ export async function uploadChatMediaBatch(params: { albumCount: items.length, ...(width ? { width } : {}), ...(height ? { height } : {}), - ...(item.sendAsFile ? { sendAsFile: true } : {}) + ...(item.sendAsFile ? { sendAsFile: true } : {}), + ...(videoNote ? { videoNote: true } : {}) }; let uploadFile = file; diff --git a/apps/frontend/lib/chat-media.ts b/apps/frontend/lib/chat-media.ts index 2e2daaa..17f6e50 100644 --- a/apps/frontend/lib/chat-media.ts +++ b/apps/frontend/lib/chat-media.ts @@ -77,6 +77,42 @@ export function resolveChatContentType(file: Pick) { return aliased || 'application/octet-stream'; } +const VIDEO_NOTE_FILE_NAME_PATTERNS = [ + /video[_-]?circle/i, + /video[_-]?note/i, + /videonote/i, + /circle[_-]?video/i, + /round[_-]?video/i, + /tg[_-]?round/i, + /кружок/i +]; + +export function isVideoNoteFileName(fileName?: string | null) { + if (!fileName?.trim()) return false; + return VIDEO_NOTE_FILE_NAME_PATTERNS.some((pattern) => pattern.test(fileName.trim())); +} + +export function isVideoNoteMessage( + message: { + type: string; + metadataJson?: string; + mimeType?: string | null; + content?: string | null; + }, + e2ePayload?: { kind?: string; fileName?: string } | null +) { + if (message.type === 'VIDEO_NOTE') return true; + if (e2ePayload?.kind === 'video_note') return true; + + const meta = parseMessageMetadata(message.metadataJson); + if (meta.videoNote) return true; + + const fileName = e2ePayload?.fileName || meta.fileName || message.content; + if (!isVideoNoteFileName(fileName)) return false; + + return message.type === 'VIDEO' || message.type === 'FILE' || message.mimeType?.startsWith('video/') === true; +} + export function inferChatMessageType(contentType: string, options?: { voice?: boolean; videoNote?: boolean }): ChatAttachmentKind { const normalized = resolveChatContentType({ type: contentType, name: '' }); if (options?.videoNote && normalized.startsWith('video/')) return 'VIDEO_NOTE'; @@ -90,7 +126,8 @@ export function inferChatMessageType(contentType: string, options?: { voice?: bo export function detectChatMessageType(file: File, options?: { voice?: boolean; videoNote?: boolean }) { const contentType = resolveChatContentType(file); - return inferChatMessageType(contentType, options); + const videoNote = Boolean(options?.videoNote || isVideoNoteFileName(file.name)); + return inferChatMessageType(contentType, { ...options, videoNote }); } export function isVideoFile(file: Pick) { diff --git a/apps/frontend/lib/chat-message-utils.ts b/apps/frontend/lib/chat-message-utils.ts index fbaae6c..8c35580 100644 --- a/apps/frontend/lib/chat-message-utils.ts +++ b/apps/frontend/lib/chat-message-utils.ts @@ -1,4 +1,5 @@ import type { ChatMessage } from '@/lib/api'; +import { isVideoNoteFileName } from '@/lib/chat-media'; import { emojiIdToNative, resolveNativeEmojiContent } from '@/lib/emoji-native-map'; export const QUICK_REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '🙏', '🔥', '👏'] as const; @@ -58,6 +59,16 @@ export function getMessagePreviewText(message: ChatMessage, visibleText?: string if (message.type === 'IMAGE') return '📷 Фото'; if (message.type === 'VIDEO') return '🎬 Видео'; if (message.type === 'VIDEO_NOTE') return '⭕ Кружок'; + if (message.type === 'VIDEO' || message.type === 'FILE') { + const meta = message.metadataJson ? (() => { + try { + return JSON.parse(message.metadataJson) as { fileName?: string; videoNote?: boolean }; + } catch { + return {}; + } + })() : {}; + if (meta.videoNote || isVideoNoteFileName(meta.fileName || message.content)) return '⭕ Кружок'; + } if (message.type === 'LOCATION') return '📍 Геопозиция'; if (message.type === 'VOICE') return '🎤 Голосовое'; if (message.type === 'AUDIO') return '🎵 Аудио';