web: improve message rendering and move things around

This commit is contained in:
Tulir Asokan 2024-10-09 00:28:36 +03:00
parent 23478caa6e
commit c048eedabe
13 changed files with 548 additions and 30 deletions

View file

@ -43,7 +43,7 @@ export default tseslint.config(
}],
"array-bracket-spacing": ["error", "never"],
"one-var-declaration-per-line": ["error", "initializations"],
"quotes": ["error", "double"],
"quotes": ["error", "double", { allowTemplateLiterals: true }],
"semi": ["error", "never"],
"comma-dangle": ["error", "always-multiline"],
"max-len": ["warn", 120],

144
web/package-lock.json generated
View file

@ -11,12 +11,15 @@
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"linkify-react": "^4.1.3",
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1"
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/sanitize-html": "^2.13.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.11.1",
"eslint-plugin-import": "^2.31.0",
@ -1044,6 +1047,15 @@
"@types/react": "*"
}
},
"node_modules/@types/sanitize-html": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.13.0.tgz",
"integrity": "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==",
"dev": true,
"dependencies": {
"htmlparser2": "^8.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
@ -1659,6 +1671,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -1705,6 +1725,68 @@
"node": ">=0.10.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
]
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-abstract": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@ -1880,7 +1962,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
@ -2503,6 +2584,24 @@
"node": ">= 0.4"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -2718,6 +2817,14 @@
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -2882,6 +2989,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/linkify-react": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.3.tgz",
"integrity": "sha512-rhI3zM/fxn5BfRPHfi4r9N7zgac4vOIxub1wHIWXLA5ENTMs+BGaIaFO1D1PhmxgwhIKmJz3H7uCP0Dg5JwSlA==",
"peerDependencies": {
"linkifyjs": "^4.0.0",
"react": ">= 15.0.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -2956,7 +3072,6 @@
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
@ -3123,6 +3238,11 @@
"node": ">=6"
}
},
"node_modules/parse-srcset": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -3150,8 +3270,7 @@
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -3178,7 +3297,6 @@
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -3415,6 +3533,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sanitize-html": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz",
"integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",
"htmlparser2": "^8.0.0",
"is-plain-object": "^5.0.0",
"parse-srcset": "^1.0.2",
"postcss": "^8.3.11"
}
},
"node_modules/scheduler": {
"version": "0.25.0-rc-0751fac7-20241002",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-0751fac7-20241002.tgz",
@ -3507,7 +3638,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}

View file

@ -13,12 +13,15 @@
"dependencies": {
"@types/react": "npm:types-react@rc",
"@types/react-dom": "npm:types-react-dom@rc",
"linkify-react": "^4.1.3",
"react": "^19.0.0-rc-0751fac7-20241002",
"react-dom": "^19.0.0-rc-0751fac7-20241002",
"react-spinners": "^0.14.1"
"react-spinners": "^0.14.1",
"sanitize-html": "^2.13.1"
},
"devDependencies": {
"@eslint/js": "^9.11.1",
"@types/sanitize-html": "^2.13.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.11.1",
"eslint-plugin-import": "^2.31.0",

28
web/src/api/media.ts Normal file
View file

@ -0,0 +1,28 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
export const getMediaURL = (mxc?: string): string | undefined => {
if (!mxc) {
return undefined
}
const match = mxc.match(mediaRegex)
if (!match) {
return undefined
}
return `_gomuks/media/${match[1]}/${match[2]}`
}

View file

@ -15,6 +15,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import type { RoomListEntry } from "../../api/statestore.ts"
import type { DBEvent } from "../../api/types/hitypes.ts"
import { getMediaURL } from "../../api/media.ts"
export interface RoomListEntryProps {
room: RoomListEntry
@ -35,24 +36,11 @@ function makePreviewText(evt?: DBEvent): string {
return ""
}
const avatarRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
const getAvatarURL = (avatar?: string): string | undefined => {
if (!avatar) {
return undefined
}
const match = avatar.match(avatarRegex)
if (!match) {
return undefined
}
return `_gomuks/media/${match[1]}/${match[2]}`
}
const Entry = ({ room, setActiveRoom }: RoomListEntryProps) => {
const previewText = makePreviewText(room.preview_event)
return <div className="room-entry" onClick={setActiveRoom} data-room-id={room.room_id}>
<div className="room-entry-left">
<img className="room-avatar" src={getAvatarURL(room.avatar)} alt=""/>
<img className="room-avatar" src={getMediaURL(room.avatar)} alt=""/>
</div>
<div className="room-entry-right">
<div className="room-name">{room.name}</div>

View file

@ -0,0 +1,10 @@
div.timeline-event {
width: 100%;
max-width: 100%;
overflow: hidden;
img {
max-width: 50%;
max-height: 25vh;
}
}

View file

@ -13,27 +13,50 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React from "react"
import { RoomStateStore } from "../../api/statestore.ts"
import { DBEvent } from "../../api/types/hitypes.ts"
import { EventContentProps } from "./content/props.ts"
import HiddenEvent from "./content/HiddenEvent.tsx"
import "./TimelineEvent.css"
import MessageBody from "./content/MessageBody.tsx"
export interface TimelineEventProps {
room: RoomStateStore
eventRowID: number
}
function getBodyType(evt: DBEvent): React.FunctionComponent<EventContentProps> {
switch (evt.type) {
case "m.room.encrypted":
switch (evt.decrypted_type) {
case "m.room.message":
return MessageBody
}
break
case "m.room.message":
return MessageBody
case "m.sticker":
}
return HiddenEvent
}
const TimelineEvent = ({ room, eventRowID }: TimelineEventProps) => {
const evt = room.eventsByRowID.get(eventRowID)
if (!evt) {
return null
}
// @ts-expect-error TODO add content types
const body = (evt.decrypted ?? evt.content).body
const BodyType = getBodyType(evt)
if (BodyType === HiddenEvent) {
return <div className="timeline-event">
<BodyType room={room} event={evt}/>
</div>
}
return <div className="timeline-event">
<code>{evt.decrypted_type ?? evt.type}</code>
&nbsp;
<code>{evt.sender}</code>
&nbsp;
{body ?? <code>{JSON.stringify(evt.decrypted ?? evt.content, null, " ")}</code>}
<div className="event-sender">
{evt.sender}
</div>
<BodyType room={room} event={evt}/>
</div>
}

View file

@ -0,0 +1,3 @@
div.timeline-view {
padding: 1rem;
}

View file

@ -18,6 +18,7 @@ import { RoomStateStore } from "../../api/statestore.ts"
import { useNonNullEventAsState } from "../../util/eventdispatcher.ts"
import { ClientContext } from "../ClientContext.ts"
import TimelineEvent from "./TimelineEvent.tsx"
import "./TimelineView.css"
interface TimelineViewProps {
room: RoomStateStore

View file

@ -0,0 +1,22 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { EventContentProps } from "./props.ts"
const HiddenEvent = ({ event }: EventContentProps) => {
return <code>{`{ "type": "${event.type}" }`}</code>
}
export default HiddenEvent

View file

@ -0,0 +1,84 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import sanitizeHtml from "sanitize-html"
import { ContentURI } from "../../../api/types/hitypes.ts"
import { getMediaURL } from "../../../api/media.ts"
import { sanitizeHtmlParams } from "../../../util/html.ts"
import { EventContentProps } from "./props.ts"
interface BaseMessageEventContent {
msgtype: string
body: string
formatted_body?: string
format?: "org.matrix.custom.html"
}
interface TextMessageEventContent extends BaseMessageEventContent {
msgtype: "m.text" | "m.notice" | "m.emote"
}
interface MediaMessageEventContent extends BaseMessageEventContent {
msgtype: "m.image" | "m.file" | "m.audio" | "m.video"
url?: ContentURI
file?: {
url: ContentURI
k: string
v: "v2"
ext: true
alg: "A256CTR"
key_ops: string[]
kty: "oct"
}
info?: {
mimetype?: string
size?: number
w?: number
h?: number
duration?: number
}
}
interface LocationMessageEventContent extends BaseMessageEventContent {
msgtype: "m.location"
geo_uri: string
}
type MessageEventContent = TextMessageEventContent | MediaMessageEventContent | LocationMessageEventContent
const MessageBody = ({ event }: EventContentProps) => {
const content = (event.decrypted ?? event.content) as MessageEventContent
switch (content.msgtype) {
case "m.text":
case "m.emote":
case "m.notice":
if (content.format === "org.matrix.custom.html") {
return <div dangerouslySetInnerHTML={{
__html: sanitizeHtml(content.formatted_body!, sanitizeHtmlParams),
}}/>
}
return content.body
case "m.image":
if (content.url) {
return <img src={getMediaURL(content.url)} alt={content.body}/>
} else if (content.file) {
return <img src={getMediaURL(content.file.url)} alt={content.body}/>
}
}
return <code>{`{ "type": "${event.type}" }`}</code>
}
export default MessageBody

View file

@ -0,0 +1,22 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { RoomStateStore } from "../../../api/statestore.ts"
import { DBEvent } from "../../../api/types/hitypes.ts"
export interface EventContentProps {
room: RoomStateStore
event: DBEvent
}

204
web/src/util/html.ts Normal file
View file

@ -0,0 +1,204 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// From matrix-react-sdk, Copyright 2024 The Matrix.org Foundation C.I.C.
// Originally licensed under the Apache License, Version 2.0
// https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/Linkify.tsx#L245
import sanitizeHtml from "sanitize-html"
import { getMediaURL } from "../api/media.ts"
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/
export const PERMITTED_URL_SCHEMES = [
"bitcoin",
"ftp",
"geo",
"http",
"https",
"im",
"irc",
"ircs",
"magnet",
"mailto",
"matrix",
"mms",
"news",
"nntp",
"openpgp4fpr",
"sip",
"sftp",
"sms",
"smsto",
"ssh",
"tel",
"urn",
"webcal",
"wtai",
"xmpp",
]
export const transformTags: NonNullable<sanitizeHtml.IOptions["transformTags"]> = {
"a": function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (attribs.href) {
attribs.target = "_blank"
} else {
// Delete the href attrib if it is falsy
delete attribs.href
}
attribs.rel = "noreferrer noopener" // https://mathiasbynens.github.io/rel-noopener/
return { tagName, attribs }
},
"img": function(tagName: string, attribs: sanitizeHtml.Attributes) {
const src = attribs.src
if (!src.startsWith("mxc://")) {
return {
tagName,
attribs: {},
}
}
const requestedWidth = Number(attribs.width)
const requestedHeight = Number(attribs.height)
const width = Math.min(requestedWidth || 800, 800)
const height = Math.min(requestedHeight || 600, 600)
// specify width/height as max values instead of absolute ones to allow object-fit to do its thing
// we only allow our own styles for this tag so overwrite the attribute
attribs.style = `max-width: ${width}px; max-height: ${height}px;`
if (requestedWidth) {
attribs.style += "width: 100%;"
}
if (requestedHeight) {
attribs.style += "height: 100%;"
}
attribs.src = getMediaURL(src)!
return { tagName, attribs }
},
"code": function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (typeof attribs.class !== "undefined") {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith("language-") && !cl.startsWith("language-_")
})
attribs.class = classes.join(" ")
}
return { tagName, attribs }
},
"*": function(tagName: string, attribs: sanitizeHtml.Attributes) {
// Delete any style previously assigned, style is an allowedTag for font, span & img,
// because attributes are stripped after transforming.
// For img this is trusted as it is generated wholly within the img transformation method.
if (tagName !== "img") {
delete attribs.style
}
// Sanitise and transform data-mx-color and data-mx-bg-color to their CSS
// equivalents
const customCSSMapper: Record<string, string> = {
"data-mx-color": "color",
"data-mx-bg-color": "background-color",
// $customAttributeKey: $cssAttributeKey
}
let style = ""
for (const [customAttributeKey, cssAttributeKey] of Object.entries(customCSSMapper)) {
const customAttributeValue = attribs[customAttributeKey]
if (
customAttributeValue &&
typeof customAttributeValue === "string" &&
COLOR_REGEX.test(customAttributeValue)
) {
style += cssAttributeKey + ":" + customAttributeValue + ";"
delete attribs[customAttributeKey]
}
}
if (style) {
attribs.style = style + (attribs.style || "")
}
return { tagName, attribs }
},
}
export const sanitizeHtmlParams: sanitizeHtml.IOptions = {
allowedTags: [
// These tags are suggested by the spec https://spec.matrix.org/v1.12/client-server-api/#mroommessage-msgtypes
"font",
"del",
"s",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"p",
"a",
"ul",
"ol",
"sup",
"sub",
"nl",
"li",
"b",
"i",
"u",
"strong",
"em",
"strike",
"code",
"hr",
"br",
"div",
"table",
"thead",
"caption",
"tbody",
"tr",
"th",
"td",
"pre",
"span",
"img",
"details",
"summary",
],
allowedAttributes: {
// attribute sanitization happens after transformations, so we have to accept `style` for font, span & img
// but strip during the transformation.
// custom ones first:
font: ["color", "data-mx-bg-color", "data-mx-color", "style"], // custom to matrix
span: ["data-mx-maths", "data-mx-bg-color", "data-mx-color", "data-mx-spoiler", "style"], // custom to matrix
div: ["data-mx-maths"],
// eslint-disable-next-line id-length
a: ["href", "name", "target", "rel"], // remote target: custom to matrix
// img tags also accept width/height, we just map those to max-width & max-height during transformation
img: ["src", "alt", "title", "style"],
ol: ["start"],
code: ["class"], // We don't actually allow all classes, we filter them in transformTags
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ["img", "br", "hr", "area", "base", "basefont", "input", "link", "meta"],
// URL schemes we permit
allowedSchemes: PERMITTED_URL_SCHEMES,
allowProtocolRelative: false,
transformTags,
// 50 levels deep "should be enough for anyone"
nestingLimit: 50,
}