@droppii-org/chat-sdk
Version:
Droppii React Chat SDK
219 lines (218 loc) • 8.46 kB
JavaScript
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;
}