import { emojiBaseUrl, emojiUrlMap } from '../constant/emoji'; import { MessageContentType } from '../types/messageInput'; import type { InputContent } from '../types/messageInput'; import type { JSONContent } from '@tiptap/react'; /** * Converts a blob URL to a File object */ async function blobUrlToFile(blobUrl: string, fileName = 'image.png', fileType = 'image/png'): Promise { try { const response = await fetch(blobUrl); const blobData = await response.blob(); return new File([blobData], fileName, { type: fileType, lastModified: Date.now() }); } catch { return null; } } /** * Converts input content to editor node format */ function convertInputContentToEditorNode(item: InputContent) { switch (item.type) { case MessageContentType.TEXT: return { type: 'text', text: item.content, }; case MessageContentType.IMAGE: { if (typeof item.content === 'string') { return { type: MessageContentType.IMAGE, attrs: { src: item.content, }, }; } return { type: MessageContentType.IMAGE, attrs: { src: URL.createObjectURL(item.content), fileData: item.content, title: item.content.name, }, }; } case MessageContentType.EMOJI: { const emoticonContent = item.content as { url: string; key: string; text: string }; return { type: MessageContentType.EMOJI, attrs: { src: emoticonContent.url, alt: emoticonContent.key, title: emoticonContent.text, }, }; } case MessageContentType.MENTION: { const mentionContent = item.content; return { type: MessageContentType.MENTION, attrs: { 'id': mentionContent.id, 'label': mentionContent.label, 'data-mention-char': mentionContent.mentionSuggestionChar, }, }; } case MessageContentType.FILE: { const { file, src, alt, title } = item.content as { file: File; src: string; alt: string; title: string }; return { type: MessageContentType.FILE, attrs: { 'file-data': file, 'src': src, 'alt': alt, 'title': title, }, }; } default: return { type: 'text', text: String(item.content), }; } } /** * Parses text and extracts emoji patterns, converting them to InputContent array */ function parseTextWithEmoji(text: string): InputContent[] { const contents: InputContent[] = []; let lastIndex = 0; const emojiKeys = Object.keys(emojiUrlMap); const escapedEmojiKeys = emojiKeys.map(key => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const emojiRegex = new RegExp(`(${escapedEmojiKeys.join('|')})`, 'g'); let match = emojiRegex.exec(text); while (match !== null) { if (match.index > lastIndex) { contents.push({ type: MessageContentType.TEXT, content: text.slice(lastIndex, match.index), }); } const emojiKey = match[0]; contents.push({ type: MessageContentType.EMOJI, content: { url: emojiBaseUrl + emojiUrlMap[emojiKey], key: emojiKey, text: emojiKey, }, }); lastIndex = match.index + match[0].length; match = emojiRegex.exec(text); } if (lastIndex < text.length) { contents.push({ type: MessageContentType.TEXT, content: text.slice(lastIndex), }); } return contents; } /** * Trims whitespace from the beginning and end of an InputContent array */ function trimInputContent(content: InputContent[]): InputContent[] { if (content.length === 0) { return []; } let firstNonEmptyIndex = -1; let lastNonEmptyIndex = -1; for (let i = 0; i < content.length; i += 1) { const item = content[i]; const isNonEmpty = item.type !== MessageContentType.TEXT || (item.content as string).trim().length > 0; if (isNonEmpty) { if (firstNonEmptyIndex === -1) { firstNonEmptyIndex = i; } lastNonEmptyIndex = i; } } if (firstNonEmptyIndex === -1) { return []; } return content .slice(firstNonEmptyIndex, lastNonEmptyIndex + 1) .map((item, index, array) => { if (item.type === MessageContentType.TEXT) { let textContent = item.content as string; if (index === 0) { textContent = textContent.trimStart(); } if (index === array.length - 1) { textContent = textContent.trimEnd(); } return { ...item, content: textContent }; } return item; }) .filter((item) => { if (item.type === MessageContentType.TEXT) { return (item.content as string).length > 0; } return true; }); } /** * Convert TipTap editor JSON to InputContent array */ function convertEditorContent(node: JSONContent): InputContent[] { if (!node?.content) { return []; } return node.content.flatMap((child: JSONContent, index) => { const type = child.attrs?.['data-type'] || child.type; switch (type) { case 'text': return child.text ? [{ type: MessageContentType.TEXT, content: child.text }] : []; case MessageContentType.IMAGE: case 'image': return [{ type: MessageContentType.IMAGE, content: child.attrs?.src }]; case MessageContentType.EMOJI: case 'emoji': return [{ type: MessageContentType.EMOJI, content: { url: child.attrs?.src, key: child.attrs?.alt, text: child.attrs?.title, }, }]; case MessageContentType.FILE: return [{ type: MessageContentType.FILE, content: child.attrs?.['file-data'] }]; case 'hardBreak': return [{ type: MessageContentType.TEXT, content: '\n' }]; case 'mention': return [{ type: MessageContentType.MENTION, content: { id: child.attrs?.id, label: child.attrs?.label, mentionSuggestionChar: child.attrs?.['data-mention-char'] || '@', }, }]; default: { const result = convertEditorContent(child); if (child.type === 'paragraph' && index > 0) { return [{ type: MessageContentType.TEXT, content: '\n' }, ...result]; } return result; } } }); } export { blobUrlToFile, convertInputContentToEditorNode, parseTextWithEmoji, trimInputContent, convertEditorContent, };