103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import type { ChatMessage } from '@/lib/api';
|
|
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 === '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));
|
|
}
|