From 5e0d225b24059d64317a113dc2740733a75bcd37 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sun, 21 Sep 2025 16:55:39 +0300 Subject: [PATCH] Implement formatting --- frontend/src/resources/css/_chat.scss | 10 ++++++- frontend/src/ui/components/chat/Message.tsx | 29 +++++++++++++++------ package.json | 1 + 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/frontend/src/resources/css/_chat.scss b/frontend/src/resources/css/_chat.scss index 2eb1864..d1c65e7 100644 --- a/frontend/src/resources/css/_chat.scss +++ b/frontend/src/resources/css/_chat.scss @@ -205,8 +205,16 @@ .message-content { word-wrap: break-word; - margin-bottom: 0.3rem; + margin-bottom: 10px; white-space: pre-wrap; + + > p:first-child { + margin-block-start: 0; + } + + > p:last-child { + margin-block-end: 0; + } } .quote.reply-preview { diff --git a/frontend/src/ui/components/chat/Message.tsx b/frontend/src/ui/components/chat/Message.tsx index ab095ad..e5835be 100644 --- a/frontend/src/ui/components/chat/Message.tsx +++ b/frontend/src/ui/components/chat/Message.tsx @@ -2,6 +2,9 @@ import { formatTime } from "../../../utils/utils"; import type { Message as MessageType } from "../../../core/types"; import defaultAvatar from "../../../resources/images/default-avatar.png"; import Quote from "../core/Quote"; +import { parse } from "marked"; +import DOMPurify from "dompurify"; +import { useEffect, useState } from "react"; interface MessageProps { message: MessageType; @@ -13,11 +16,23 @@ interface MessageProps { } export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false, isDm = false }: MessageProps) { - const handleContextMenu = (e: React.MouseEvent) => { + const [formattedMessage, setFormattedMessage] = useState({ __html: DOMPurify.sanitize(message.content).trim() }); + + useEffect(() => { + (async () => { + setFormattedMessage({ + __html: DOMPurify.sanitize( + await parse(message.content) + ).trim() + }); + })(); + }, [message]); + + function handleContextMenu(e: React.MouseEvent) { e.preventDefault(); e.stopPropagation(); onContextMenu(e, message); - }; + } return (
!isLoadingProfile && onProfileClick(message.username)} - style={{ cursor: isLoadingProfile ? "default" : "pointer" }}> {/* TODO extract to SCSS */} + style={{ cursor: isLoadingProfile ? "default" : "pointer" }}> {message.username}
)} @@ -60,17 +75,15 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLo )} -
- {message.content} -
+
{formatTime(message.timestamp)} {message.is_edited ? " (edited)" : undefined} - {isAuthor && message.is_read ? ( + {isAuthor && message.is_read && ( - ) : undefined} + )}
diff --git a/package.json b/package.json index a5d36a7..f145564 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "vite-plugin-html": "^3.2.2" }, "dependencies": { + "dompurify": "^3.2.7", "electron-squirrel-startup": "^1.0.1", "marked": "^16.3.0", "mdui": "^2.1.4",