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 && (
+
+ )}