Files
IdP/apps/frontend/lib/chat-message-utils.ts
lendry a0966e7ba2 fix
2026-07-10 12:14:06 +03:00

114 lines
4.2 KiB
TypeScript

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;
export interface ForwardedFromMeta {
messageId: string;
roomId: string;
senderId: string;
senderName: string;
type: string;
content?: string;
}
export function parseForwardedFrom(metadataJson?: string): ForwardedFromMeta | null {
if (!metadataJson) return null;
try {
const meta = JSON.parse(metadataJson) as { forwardedFrom?: ForwardedFromMeta };
const forwarded = meta.forwardedFrom;
return forwarded?.messageId ? forwarded : null;
} catch {
return null;
}
}
export function getMessageCopyText(message: ChatMessage, visibleText?: string) {
if (message.isDeleted) return '';
if (visibleText?.trim()) return resolveNativeEmojiContent(visibleText.trim());
if (message.content?.trim()) {
if (message.type === 'EMOJI') {
return emojiIdToNative(message.content.trim()) ?? message.content.trim();
}
return resolveNativeEmojiContent(message.content.trim());
}
if (message.type === 'POLL' && message.poll) return message.poll.question;
return '';
}
function looksLikeEncryptedPayload(content?: string | null) {
if (!content?.trim().startsWith('{')) return false;
try {
const parsed = JSON.parse(content) as { v?: unknown; ciphertext?: unknown; kind?: unknown };
return typeof parsed === 'object' && parsed !== null && ('v' in parsed || 'ciphertext' in parsed || 'kind' in parsed);
} catch {
return false;
}
}
export function getMessagePreviewText(message: ChatMessage, visibleText?: string) {
if (message.type === 'SYSTEM') {
return resolveNativeEmojiContent(message.content?.trim() ?? 'Системное сообщение');
}
if (message.type === 'POLL' && message.poll) {
return `📊 Опрос: ${message.poll.question}`;
}
const text = getMessageCopyText(message, visibleText);
if (text && !looksLikeEncryptedPayload(text)) return text;
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 '🎵 Аудио';
if (message.type === 'FILE') return '📄 Файл';
if (message.isEncrypted) return '🔒 Зашифрованное сообщение';
return 'Сообщение';
}
export function getChatListPreviewText(message?: ChatMessage | null, fallback = '') {
if (!message || message.isDeleted) return fallback;
return getMessagePreviewText(message);
}
export function isSystemMessage(message: ChatMessage) {
return message.type === 'SYSTEM';
}
export function isSelectableMessage(message: ChatMessage) {
return !message.isDeleted && !isSystemMessage(message);
}
export function isChatInteractiveTarget(target: EventTarget | null) {
if (!(target instanceof HTMLElement)) return false;
return Boolean(
target.closest('button, a, input, textarea, select, label, [contenteditable="true"], [data-chat-interactive="true"]')
);
}
export function canEditMessage(message: ChatMessage, userId?: string, isE2E?: boolean) {
return (
Boolean(userId && message.senderId === userId) &&
!message.isDeleted &&
!isSystemMessage(message) &&
!message.isEncrypted &&
!isE2E &&
(message.type === 'TEXT' || message.type === 'EMOJI')
);
}
export function canDeleteMessage(message: ChatMessage, userId?: string) {
return Boolean(userId && message.senderId === userId && !message.isDeleted && !isSystemMessage(message));
}