diff --git a/package-lock.json b/package-lock.json index e7a4e988..8db20b20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,15 +18,15 @@ "@fortawesome/react-fontawesome": "^3.1.1", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.2.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", "@tanstack/react-query": "^5.90.16", "@tanstack/react-query-devtools": "^5.91.2", + "@types/dompurify": "^3.0.5", "@types/react": "^18.3.10", "dayjs": "^1.11.13", + "dompurify": "^3.3.1", "emoji-mart": "^5.6.0", "jwt-decode": "^4.0.0", - "linkify-react": "^4.3.2", - "linkifyjs": "^4.3.2", + "marked": "^17.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.2", @@ -148,6 +148,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -496,6 +497,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -539,6 +541,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -601,6 +604,7 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -640,6 +644,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1231,6 +1236,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1402,6 +1408,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -1962,6 +1969,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.16" }, @@ -1996,6 +2004,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2149,6 +2158,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "dev": true, @@ -2216,6 +2234,7 @@ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2233,6 +2252,7 @@ "node_modules/@types/react": { "version": "18.3.10", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2242,6 +2262,7 @@ "version": "18.3.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" } @@ -2272,6 +2293,12 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.0", "license": "MIT" @@ -2319,6 +2346,7 @@ "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", @@ -2728,6 +2756,7 @@ "version": "8.11.3", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3718,6 +3747,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4433,6 +4463,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -4481,7 +4520,8 @@ }, "node_modules/emoji-mart": { "version": "5.6.0", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/entities": { "version": "4.5.0", @@ -5371,6 +5411,7 @@ "version": "8.57.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6643,6 +6684,7 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -6783,22 +6825,6 @@ "version": "1.2.4", "license": "MIT" }, - "node_modules/linkify-react": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.3.2.tgz", - "integrity": "sha512-mi744h1hf+WDsr+paJgSBBgYNLMWNSHyM9V9LVUo03RidNGdw1VpI7Twnt+K3pEh3nIzB4xiiAgZxpd61ItKpQ==", - "license": "MIT", - "peerDependencies": { - "linkifyjs": "^4.0.0", - "react": ">= 15.0.0" - } - }, - "node_modules/linkifyjs": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", - "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -6911,6 +6937,18 @@ "node": ">=10" } }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "dev": true, @@ -7874,6 +7912,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7907,6 +7946,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8477,6 +8517,7 @@ "node_modules/react": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8487,6 +8528,7 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10877,6 +10919,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11058,6 +11101,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11133,6 +11177,7 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", @@ -11543,21 +11588,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -11575,6 +11605,7 @@ "integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index ce13b11f..7986685b 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,13 @@ "@mui/material": "^7.2.0", "@tanstack/react-query": "^5.90.16", "@tanstack/react-query-devtools": "^5.91.2", + "@types/dompurify": "^3.0.5", "@types/react": "^18.3.10", "dayjs": "^1.11.13", + "dompurify": "^3.3.1", "emoji-mart": "^5.6.0", "jwt-decode": "^4.0.0", - "linkify-react": "^4.3.2", - "linkifyjs": "^4.3.2", + "marked": "^17.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.2", diff --git a/src/components/message/Links.test.tsx b/src/components/message/Links.test.tsx deleted file mode 100644 index 1af2301d..00000000 --- a/src/components/message/Links.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { render } from '@testing-library/react' -import Links from './Links' - -describe('Links', () => { - it('should render links', () => { - const message = 'https://example.com/' - - render() - - expect(document.querySelector('a')).toHaveAttribute('href', 'https://example.com/') - expect(document.querySelector('a')).toHaveTextContent('example.com') - }) - - it('should render links without protocol', () => { - const message = 'example.com' - - render() - - expect(document.querySelector('a')).toHaveAttribute('href', 'https://example.com') - expect(document.querySelector('a')).toHaveTextContent('example.com') - }) - - it('should shorten long links', () => { - const message = 'https://www.systemli.org/2024/01/11/neue-sicherheitsma%C3%9Fnahme-gegen-mitm-angriffe-eingef%C3%BChrt/' - - render() - - expect(document.querySelector('a')).toHaveAttribute( - 'href', - 'https://www.systemli.org/2024/01/11/neue-sicherheitsma%C3%9Fnahme-gegen-mitm-angriffe-eingef%C3%BChrt/' - ) - expect(document.querySelector('a')).toHaveTextContent('www.systemli.org/2024/01/11/ne…') - }) -}) diff --git a/src/components/message/Links.tsx b/src/components/message/Links.tsx deleted file mode 100644 index db0cb67d..00000000 --- a/src/components/message/Links.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Linkify from 'linkify-react' -import { FC } from 'react' - -interface Props { - message: string -} -const Links: FC = ({ message }) => { - const format = (value: string, type: string) => { - if (type === 'url') { - value = value.replace(/https?:\/\//, '') - return value.length > 30 ? `${value.slice(0, 30)}…` : value - } - return value - } - - return {message} -} - -export default Links diff --git a/src/components/message/Message.test.tsx b/src/components/message/Message.test.tsx index 4770d0d3..e39772ad 100644 --- a/src/components/message/Message.test.tsx +++ b/src/components/message/Message.test.tsx @@ -9,7 +9,7 @@ describe('Message', () => { id: 1, ticker: 1, createdAt: '2021-10-01T00:00:00Z', - text: 'Multi line message with links\nhttps://example.com\nhttps://example.net', + text: '# Header\nMulti line message with links\nhttps://example.com\nhttps://example.net\n**and bold text**', attachments: [ { contentType: 'image/jpeg', @@ -23,9 +23,11 @@ describe('Message', () => { renderWithProviders() + expect(screen.getByRole('heading')).toBeInTheDocument() expect(screen.getByText('Multi line message with links')).toBeInTheDocument() - expect(screen.getByText('example.com')).toBeInTheDocument() - expect(screen.getByText('example.net')).toBeInTheDocument() + expect(screen.getByText('https://example.com')).toBeInTheDocument() + expect(screen.getByText('https://example.net')).toBeInTheDocument() + expect(screen.getByRole('strong')).toBeInTheDocument() expect(screen.getByRole('img')).toBeInTheDocument() }) }) diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx index 033788ac..dc810cbf 100644 --- a/src/components/message/Message.tsx +++ b/src/components/message/Message.tsx @@ -2,11 +2,19 @@ import { Close } from '@mui/icons-material' import { Box, Card, CardContent, IconButton, useTheme } from '@mui/material' import { FC, useState } from 'react' import { Message as MessageType } from '../../api/Message' -import Links from './Links' +import DOMPurify from 'dompurify' +import { marked } from 'marked' import MessageAttachements from './MessageAttachments' import MessageFooter from './MessageFooter' import MessageModalDelete from './MessageModalDelete' +// Configure marked to support GitHub Flavored Markdown +marked.setOptions({ + gfm: true, + breaks: true, + pedantic: false, +}) + interface Props { message: MessageType } @@ -15,6 +23,10 @@ const Message: FC = ({ message }) => { const theme = useTheme() const [deleteModalOpen, setDeleteModalOpen] = useState(false) + // Process the description with Markdown and sanitize the result + const processedMessage = message.text ? + DOMPurify.sanitize(marked.parse(message.text)) : null + return ( @@ -27,11 +39,12 @@ const Message: FC = ({ message }) => { setDeleteModalOpen(false)} open={deleteModalOpen} /> - {message.text.split(/\r\n|\r|\n/g).map((line, i) => ( - - - - ))} + {processedMessage && ( + + )}