fix
This commit is contained in:
@@ -148,6 +148,7 @@ import {
|
|||||||
detectChatMessageType,
|
detectChatMessageType,
|
||||||
fileIconLabel,
|
fileIconLabel,
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
|
isVideoNoteMessage,
|
||||||
parseLocationMetadata,
|
parseLocationMetadata,
|
||||||
parseMessageMetadata,
|
parseMessageMetadata,
|
||||||
resolveChatContentType
|
resolveChatContentType
|
||||||
@@ -2182,6 +2183,16 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isVideoNoteMessage(message, e2ePayload)) {
|
||||||
|
const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs;
|
||||||
|
return (
|
||||||
|
<ChatVideoNotePlayer
|
||||||
|
src={media.blobUrl}
|
||||||
|
durationMs={typeof metaDuration === 'number' ? metaDuration : undefined}
|
||||||
|
variant={mine ? 'mine' : 'theirs'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (message.type === 'VIDEO') {
|
if (message.type === 'VIDEO') {
|
||||||
const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs;
|
const metaDuration = e2ePayload?.durationMs ?? parseMessageMetadata(message.metadataJson).durationMs;
|
||||||
return (
|
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 (
|
|
||||||
<ChatVideoNotePlayer
|
|
||||||
src={media.blobUrl}
|
|
||||||
durationMs={typeof metaDuration === 'number' ? metaDuration : undefined}
|
|
||||||
variant={mine ? 'mine' : 'theirs'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export function PinLockModal({ open, isSubmitting, error, onSubmit, onLogout }:
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={() => undefined}>
|
<Dialog open={open} onOpenChange={() => undefined}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="relative !top-[max(1.5rem,10vh)] !-translate-y-0 [&>button]:hidden"
|
placement="upper"
|
||||||
|
className="relative [&>button]:hidden"
|
||||||
onPointerDownOutside={(event) => event.preventDefault()}
|
onPointerDownOutside={(event) => event.preventDefault()}
|
||||||
onEscapeKeyDown={(event) => event.preventDefault()}
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
@@ -74,6 +75,19 @@ export function PinLockModal({ open, isSubmitting, error, onSubmit, onLogout }:
|
|||||||
<Button type="submit" size="lg" className="w-full rounded-[18px]" disabled={isSubmitting || !isPinInputComplete(pin)}>
|
<Button type="submit" size="lg" className="w-full rounded-[18px]" disabled={isSubmitting || !isPinInputComplete(pin)}>
|
||||||
{isSubmitting ? 'Проверяем...' : 'Разблокировать'}
|
{isSubmitting ? 'Проверяем...' : 'Разблокировать'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{onLogout ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
className="w-full rounded-[18px] text-[#6b6f7b]"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={onLogout}
|
||||||
|
>
|
||||||
|
<DoorOpen className="mr-2 h-4 w-4" />
|
||||||
|
Выйти из аккаунта
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -7,11 +7,41 @@ import { cn } from '@/lib/utils';
|
|||||||
export const Dialog = DialogPrimitive.Root;
|
export const Dialog = DialogPrimitive.Root;
|
||||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
export function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
export function DialogContent({
|
||||||
|
className,
|
||||||
|
placement = 'center',
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
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 (
|
return (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
<DialogPrimitive.Overlay className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-[1px]" />
|
<DialogPrimitive.Overlay className={cn('fixed inset-0 bg-black/50 backdrop-blur-[1px]', overlayZ)} />
|
||||||
<DialogPrimitive.Content className={cn('fixed left-1/2 top-1/2 z-[201] w-[min(92vw,520px)] -translate-x-1/2 -translate-y-1/2 rounded-[28px] bg-white p-6 shadow-2xl', className)} {...props}>
|
<DialogPrimitive.Content
|
||||||
|
className={cn('fixed w-[min(92vw,520px)] rounded-[28px] bg-white p-6 shadow-2xl', contentZ, className)}
|
||||||
|
style={{ ...placementStyle, ...style }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-5 top-5 rounded-full p-1 hover:bg-[#f4f5f8]" aria-label="Закрыть">
|
<DialogPrimitive.Close className="absolute right-5 top-5 rounded-full p-1 hover:bg-[#f4f5f8]" aria-label="Закрыть">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type ChatMessage
|
type ChatMessage
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { compressChatImage, isCompressibleImageFile, readImageDimensions } from '@/lib/chat-image-compress';
|
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';
|
import { e2eKindFromMessageType, encryptE2EBlob, type E2EMessagePayload } from '@/lib/e2e-chat';
|
||||||
|
|
||||||
export interface ChatMediaUploadItem {
|
export interface ChatMediaUploadItem {
|
||||||
@@ -54,6 +54,7 @@ export async function uploadChatMediaBatch(params: {
|
|||||||
|
|
||||||
const contentType = resolveChatContentType(file);
|
const contentType = resolveChatContentType(file);
|
||||||
const messageType = item.sendAsFile && isCompressibleImageFile(item.file) ? 'FILE' : detectChatMessageType(file);
|
const messageType = item.sendAsFile && isCompressibleImageFile(item.file) ? 'FILE' : detectChatMessageType(file);
|
||||||
|
const videoNote = messageType === 'VIDEO_NOTE' || isVideoNoteFileName(item.file.name);
|
||||||
const metadata = {
|
const metadata = {
|
||||||
fileName: item.file.name,
|
fileName: item.file.name,
|
||||||
fileSize: file.size,
|
fileSize: file.size,
|
||||||
@@ -62,7 +63,8 @@ export async function uploadChatMediaBatch(params: {
|
|||||||
albumCount: items.length,
|
albumCount: items.length,
|
||||||
...(width ? { width } : {}),
|
...(width ? { width } : {}),
|
||||||
...(height ? { height } : {}),
|
...(height ? { height } : {}),
|
||||||
...(item.sendAsFile ? { sendAsFile: true } : {})
|
...(item.sendAsFile ? { sendAsFile: true } : {}),
|
||||||
|
...(videoNote ? { videoNote: true } : {})
|
||||||
};
|
};
|
||||||
|
|
||||||
let uploadFile = file;
|
let uploadFile = file;
|
||||||
|
|||||||
@@ -77,6 +77,42 @@ export function resolveChatContentType(file: Pick<File, 'type' | 'name'>) {
|
|||||||
return aliased || 'application/octet-stream';
|
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 {
|
export function inferChatMessageType(contentType: string, options?: { voice?: boolean; videoNote?: boolean }): ChatAttachmentKind {
|
||||||
const normalized = resolveChatContentType({ type: contentType, name: '' });
|
const normalized = resolveChatContentType({ type: contentType, name: '' });
|
||||||
if (options?.videoNote && normalized.startsWith('video/')) return 'VIDEO_NOTE';
|
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 }) {
|
export function detectChatMessageType(file: File, options?: { voice?: boolean; videoNote?: boolean }) {
|
||||||
const contentType = resolveChatContentType(file);
|
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<File, 'type' | 'name'>) {
|
export function isVideoFile(file: Pick<File, 'type' | 'name'>) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ChatMessage } from '@/lib/api';
|
import type { ChatMessage } from '@/lib/api';
|
||||||
|
import { isVideoNoteFileName } from '@/lib/chat-media';
|
||||||
import { emojiIdToNative, resolveNativeEmojiContent } from '@/lib/emoji-native-map';
|
import { emojiIdToNative, resolveNativeEmojiContent } from '@/lib/emoji-native-map';
|
||||||
|
|
||||||
export const QUICK_REACTIONS = ['👍', '❤️', '😂', '😮', '😢', '🙏', '🔥', '👏'] as const;
|
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 === 'IMAGE') return '📷 Фото';
|
||||||
if (message.type === 'VIDEO') return '🎬 Видео';
|
if (message.type === 'VIDEO') return '🎬 Видео';
|
||||||
if (message.type === 'VIDEO_NOTE') 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 === 'LOCATION') return '📍 Геопозиция';
|
||||||
if (message.type === 'VOICE') return '🎤 Голосовое';
|
if (message.type === 'VOICE') return '🎤 Голосовое';
|
||||||
if (message.type === 'AUDIO') return '🎵 Аудио';
|
if (message.type === 'AUDIO') return '🎵 Аудио';
|
||||||
|
|||||||
Reference in New Issue
Block a user