diff --git a/src/channel/bubbleMsgUtils.js b/src/channel/bubbleMsgUtils.js index 88c4681..6d62870 100644 --- a/src/channel/bubbleMsgUtils.js +++ b/src/channel/bubbleMsgUtils.js @@ -1071,3 +1071,120 @@ export const phoneNumberToWAID = (input) => { } export const uploadProgressSimulate = () => fixTo2Decimals(Math.random() * (0.8 - 0.2) + 0.2); + +// Parse text segments for URLs and numbers +const parseTextForMarkdown = (text) => { + // Find URLs and four-digit numbers + const urlRegex = /https?:\/\/[^\s]+/g; + const numberRegex = /\d{4,}/g; + + const matches = []; + + // Find all URLs + let match; + while ((match = urlRegex.exec(text)) !== null) { + matches.push({ start: match.index, end: match.index + match[0].length, type: 'url', content: match[0] }); + } + + // Find all 4+ digit numbers + numberRegex.lastIndex = 0; // Reset regex + while ((match = numberRegex.exec(text)) !== null) { + matches.push({ start: match.index, end: match.index + match[0].length, type: 'number', content: match[0] }); + } + + // Sort matches by position + matches.sort((a, b) => a.start - b.start); + + // Remove overlapping matches (URLs take priority) + const filteredMatches = []; + for (const current of matches) { + const isOverlapping = filteredMatches.some(existing => + (current.start >= existing.start && current.start < existing.end) || + (current.end > existing.start && current.end <= existing.end) + ); + if (!isOverlapping) { + filteredMatches.push(current); + } + } + + // Split text into segments + const segments = []; + let currentIndex = 0; + + for (const match of filteredMatches) { + if (currentIndex < match.start) { + const textContent = text.slice(currentIndex, match.start); + if (textContent) { + segments.push({ type: 'text', content: textContent }); + } + } + segments.push(match); + currentIndex = match.end; + } + + if (currentIndex < text.length) { + const textContent = text.slice(currentIndex); + if (textContent) { + segments.push({ type: 'text', content: textContent }); + } + } + + return segments.length > 0 ? segments : [{ type: 'text', content: text }]; + }; + +// Parse markdown with nesting support, autolinks, and number recognition +export const parseSimpleMarkdown = (text) => { + const tokens = []; + let current = ''; + let i = 0; + + while (i < text.length) { + const char = text[i]; + + if (char === '*' || char === '_') { + // Save any accumulated text before processing markdown + if (current) { + tokens.push(...parseTextForMarkdown(current)); + current = ''; + } + + // Find the closing marker + const marker = char; + let j = i + 1; + let content = ''; + let found = false; + + while (j < text.length) { + if (text[j] === marker) { + found = true; + break; + } + content += text[j]; + j++; + } + + if (found && content) { + // Recursively parse the content inside the markers + tokens.push({ + type: marker === '*' ? 'bold' : 'italic', + content: parseSimpleMarkdown(content) + }); + i = j + 1; // Skip past the closing marker + } else { + // If no closing marker found, treat as regular text + current += char; + i++; + } + } else { + current += char; + i++; + } + } + + // Add any remaining text + if (current) { + tokens.push(...parseTextForMarkdown(current)); + } + + return tokens; + }; diff --git a/src/views/Conversations/History/MessagesList.jsx b/src/views/Conversations/History/MessagesList.jsx index d88a8ab..58832bf 100644 --- a/src/views/Conversations/History/MessagesList.jsx +++ b/src/views/Conversations/History/MessagesList.jsx @@ -10,6 +10,7 @@ import MergeConversationTo from './MergeConversationTo'; import BubbleIM from '../Online/Components/BubbleIM'; import BubbleEmail from '../Online/Components/BubbleEmail'; import { ERROR_IMG, POPUP_FEATURES } from '@/config'; +import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils'; const BIG_PAGE_SIZE = MESSAGE_PAGE_SIZE * 20; const MessagesList = ({ ...listProps }) => { @@ -148,6 +149,27 @@ const MessagesList = ({ ...listProps }) => { setFocusMsg(id); } }; + // Render parsed tokens to React elements + const renderMDTokens = (tokens) => { + return tokens.map((token, index) => { + switch (token.type) { + case 'text': + return {token.content}; + case 'bold': + return {renderMDTokens(token.content)}; + case 'italic': + return {renderMDTokens(token.content)}; + case 'url': + return ( + + {token.content} + + ) + default: + return {token.content}; + } + }); + }; const RenderText = memo(function renderText({ str, className, template }) { let headerObj, footerObj, buttonsArr; @@ -157,23 +179,8 @@ const MessagesList = ({ ...listProps }) => { footerObj = componentsObj?.footer?.[0]; buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []); } - - const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation})/gmu).filter((s) => s !== ''); - const links = str.match(/https?:\/\/[^\s()]+/gi) || []; - const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || []; - const extraClass = isEmpty(emojis) ? '' : ''; - const objArr = parts.reduce((prev, curr, index) => { - if (links.includes(curr)) { - prev.push({ type: 'link', key: curr }); - } else if (emojis.includes(curr)) { - prev.push({ type: 'emoji', key: curr }); - } else { - prev.push({ type: 'text', key: curr }); - } - return prev; - }, []); return ( - + {headerObj ? (
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() &&
{headerObj.text}
} @@ -185,17 +192,7 @@ const MessagesList = ({ ...listProps }) => { )}
) : null} - {(objArr || []).map((part, index) => { - if (part.type === 'link') { - return ( - - {part.key} - - ); - } else { - return part.key; - } - })} + {renderMDTokens(parseSimpleMarkdown(str))}
); }); diff --git a/src/views/Conversations/Online/Components/BubbleIM.jsx b/src/views/Conversations/Online/Components/BubbleIM.jsx index 18c04fc..6498092 100644 --- a/src/views/Conversations/Online/Components/BubbleIM.jsx +++ b/src/views/Conversations/Online/Components/BubbleIM.jsx @@ -3,6 +3,7 @@ import { App, Button, Image } from 'antd'; import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons'; import { MessageBox } from 'react-chat-elements'; import { groupBy, isEmpty, TagColorStyle } from '@/utils/commons'; +import { parseSimpleMarkdown } from '@/channel/bubbleMsgUtils'; import useConversationStore from '@/stores/ConversationStore'; import { useShallow } from 'zustand/react/shallow'; import { ReplyIcon } from '@/components/Icons'; @@ -23,6 +24,34 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s setNewChatModalVisible(true); setNewChatFormValues((prev) => ({ ...prev, phone_number: wa_id, name: wa_name })); }; + // Render parsed tokens to React elements + const renderMDTokens = (tokens) => { + return tokens.map((token, index) => { + switch (token.type) { + case 'text': + return {token.content} + case 'bold': + return {renderMDTokens(token.content)} + case 'italic': + return {renderMDTokens(token.content)} + case 'url': + return ( + + {token.content} + + ) + case 'number': + return ( + openNewChatModal({ wa_id: token.content, wa_name: token.content })}> + {token.content} + + ) + default: + return {token.content} + } + }) + }; + const RenderText = memo(function renderText({ str, className, template, message }) { let headerObj, footerObj, buttonsArr; if (!isEmpty(template) && !isEmpty(template.components)) { @@ -32,26 +61,8 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s buttonsArr = componentsObj?.button; // ?.reduce((r, c) => r.concat(c.buttons), []); } - const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== ''); - const links = str.match(/https?:\/\/[^\s()]+/gi) || []; - const numbers = str.match(/\d{4,}/g) || []; - const emojis = str.match(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g) || []; - const extraClass = isEmpty(emojis) ? '' : ''; - const objArr = parts.reduce((prev, curr, index) => { - if (links.includes(curr)) { - prev.push({ type: 'link', key: curr }); - } else if (numbers.includes(curr)) { - prev.push({ type: 'number', key: curr }); - } else if (emojis.includes(curr)) { - prev.push({ type: 'emoji', key: curr }); - } else { - prev.push({ type: 'text', key: curr }); - } - return prev; - }, []); - return ( - +
{headerObj ? (
{'text' === (headerObj?.parameters?.[0]?.type || '').toLowerCase() &&
{headerObj.text}
} @@ -63,24 +74,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s )}
) : null} - {(objArr || []).map((part, index) => { - if (part.type === 'link') { - return ( - - {part.key} - - ) - } else if (part.type === 'number') { - return ( - openNewChatModal({ wa_id: part.key, wa_name: part.key })}> - {part.key} - - ) - } else { - // if (part.type === 'emoji') - return part.key - } - })} + {renderMDTokens(parseSimpleMarkdown(str))} {footerObj ?
{footerObj.text}
: null} {buttonsArr && buttonsArr.length > 0 ? (
@@ -101,7 +95,7 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s )}
) : null} - +
) }); return (