UNPKG

@droppii-org/chat-sdk

Version:

Droppii React Chat SDK

219 lines (218 loc) 8.46 kB
import { MessageType } from "@openim/wasm-client-sdk"; import dayjs from "dayjs"; import DOMPurify from "dompurify"; export function renderFileSize(bytes) { if (!bytes || bytes <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; let index = 0; let size = bytes; while (size >= 1024 && index < units.length - 1) { size /= 1024; index++; } return `${size.toFixed(1)} ${units[index]}`; } export const generateContentBasedOnMessageType = (contentType, plainText) => { switch (contentType) { case MessageType.TextMessage: return plainText || ""; case MessageType.PictureMessage: return `[Hình ảnh]`; case MessageType.VoiceMessage: return `[Tin nhắn thoại]`; case MessageType.VideoMessage: return `[Video]`; case MessageType.FileMessage: return `[File đính kèm]`; case MessageType.UrlTextMessage: return `[Liên kết]`; default: return ""; } }; export const parseLatestMessage = (latestMsg, currentUserId, t) => { var _a, _b; if (!latestMsg) return ""; try { const msgData = JSON.parse(latestMsg); const contentType = msgData === null || msgData === void 0 ? void 0 : msgData.contentType; const isMe = currentUserId && msgData.sendID === currentUserId; const sender = isMe ? t("you") : t("customer"); switch (contentType) { case MessageType.TextMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType, (_a = msgData === null || msgData === void 0 ? void 0 : msgData.textElem) === null || _a === void 0 ? void 0 : _a.content)}`; case MessageType.PictureMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType)}`; case MessageType.VoiceMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType)}`; case MessageType.VideoMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType)}`; case MessageType.FileMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType)}`; case MessageType.UrlTextMessage: return `${sender}: ${generateContentBasedOnMessageType(contentType)}`; case MessageType.CustomMessage: { const customData = (_b = msgData.customElem) === null || _b === void 0 ? void 0 : _b.data; if (customData === "#SESSION - START") { return "Phiên chat đã bắt đầu"; } if (customData === "#SESSION - END") { return "Phiên chat đã kết thúc"; } return; } default: return "Tin nhắn không khả dụng"; } } catch (error) { console.error("Error parsing latest message:", error); return ""; } }; export const highlightSearch = (text, keyword, maxLength = 30) => { if (!keyword) return text; const lowerText = text.toLowerCase(); const lowerKeyword = keyword.toLowerCase(); const index = lowerText.indexOf(lowerKeyword); if (index === -1) { return text; // không tìm thấy } const before = text.slice(0, index); const match = text.slice(index, index + keyword.length); const after = text.slice(index + keyword.length); // 🔹 keyword dài hơn maxLength -> chỉ lấy maxLength ký tự trong keyword if (keyword.length >= maxLength) { return `<mark>${match.slice(0, maxLength)}</mark>`; } const remain = maxLength - match.length; let left = Math.floor(remain / 2); let right = remain - left; // nếu before không đủ left -> dồn cho after if (before.length < left) { right += left - before.length; left = before.length; } // nếu after không đủ right -> dồn cho before if (after.length < right) { left += right - after.length; right = after.length; } const displayedBefore = before.length > left ? "…" + before.slice(before.length - left) : before; const displayedAfter = after.length > right ? after.slice(0, right) + "…" : after; return `${displayedBefore}<mark>${match}</mark>${displayedAfter}`; }; export function formatTimestamp(timestamp, options) { const { hasTime = false, dateMonthFormat = "DD/MM" } = options || {}; const date = dayjs(timestamp); const now = dayjs(); if (date.isSame(now, "day")) { // hôm nay return hasTime ? date.format("HH:mm") : date.format(dateMonthFormat); } if (date.isSame(now, "year")) { // cùng năm return hasTime ? date.format(`HH:mm ${dateMonthFormat}`) : date.format(dateMonthFormat); } // khác năm return hasTime ? date.format(`HH:mm ${dateMonthFormat} YYYY`) : date.format(`${dateMonthFormat} YYYY`); } export const urlRegex = /\bhttps?:\/\/[^\s<>"']*[^\s<>"',.!?()\[\]{}]/g; export function extractLinks(text) { return text.match(urlRegex) || []; } export function wrapLinksInHtml(htmlContent) { var _a; // Regex để kiểm tra xem link có nằm trong thẻ <a> không const anchorTagRegex = /<a\s[^>]*>.*?<\/a>/gi; let result = ""; let lastIndex = 0; // Tách các đoạn đã có <a> sẵn ra để tránh xử lý nhầm const matches = [...htmlContent.matchAll(anchorTagRegex)]; for (const match of matches) { const matchStart = (_a = match.index) !== null && _a !== void 0 ? _a : 0; const matchEnd = matchStart + match[0].length; // Xử lý phần nằm trước thẻ a const before = htmlContent.slice(lastIndex, matchStart); const replacedBefore = before.replace(urlRegex, (url) => { return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`; }); result += replacedBefore; // Giữ nguyên phần trong thẻ a result += match[0]; lastIndex = matchEnd; } // Xử lý phần còn lại sau thẻ a cuối cùng const after = htmlContent.slice(lastIndex); const replacedAfter = after.replace(urlRegex, (url) => { return `<a class="text-blue-500 underline" href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`; }); result += replacedAfter; return result; } export const getHostFromUrl = (url) => { try { const u = new URL(url); return u.host; } catch (err) { return null; } }; /** * Sanitizes HTML content to prevent XSS attacks * Uses DOMPurify to remove all potentially malicious code * * @param html - Raw HTML content that may contain malicious scripts * @returns Sanitized HTML safe for rendering * * @example * const userInput = '<img src=x onerror=alert("XSS")>'; * const safe = sanitizeHtml(userInput); * // Returns: '<img src="x">' (onerror removed) */ export function sanitizeHtml(html) { if (!html) return ""; // Configure DOMPurify hooks DOMPurify.addHook("afterSanitizeAttributes", (node) => { // Ensure all links open in new tab with security attributes if (node.tagName === "A") { node.setAttribute("target", "_blank"); node.setAttribute("rel", "noopener noreferrer"); } }); const sanitized = DOMPurify.sanitize(html, { ALLOWED_TAGS: [ // Text formatting "b", "i", "u", "strong", "em", "mark", "small", "del", "ins", "sub", "sup", // Structure "p", "br", "div", "span", "blockquote", "pre", "code", // Lists "ul", "ol", "li", // Links "a", // Headers "h1", "h2", "h3", "h4", "h5", "h6", ], ALLOWED_ATTR: [ "href", "target", "rel", "class", "style", // Allow style for basic formatting ], ALLOW_DATA_ATTR: false, // Prevent data-* attributes ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, }); // Remove hooks after sanitization DOMPurify.removeHook("afterSanitizeAttributes"); return sanitized; }