web/timeline: ensure images don't change size when loaded

This commit is contained in:
Tulir Asokan 2024-10-10 20:19:39 +03:00
parent 947ce07d1f
commit 33f67b65a8
5 changed files with 89 additions and 17 deletions

View file

@ -2,9 +2,11 @@ div.timeline-event {
width: 100%;
max-width: 100%;
overflow: hidden;
}
img {
max-width: 50%;
max-height: 25vh;
div.media-container {
> img {
max-width: 100%;
max-height: 100%;
}
}

View file

@ -29,8 +29,8 @@ export interface TimelineEventProps {
function getBodyType(evt: DBEvent): React.FunctionComponent<EventContentProps> {
switch (evt.type) {
case "m.room.message":
return MessageBody
case "m.sticker":
return MessageBody
}
return HiddenEvent
}

View file

@ -13,12 +13,13 @@
//
// 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 { CSSProperties } from "react"
import sanitizeHtml from "sanitize-html"
import { getMediaURL } from "../../../api/media.ts"
import { ContentURI } from "../../../api/types"
import { sanitizeHtmlParams } from "../../../util/html.ts"
import { EventContentProps } from "./props.ts"
import { calculateMediaSize } from "../../../util/mediasize.ts"
interface BaseMessageEventContent {
msgtype: string
@ -61,6 +62,9 @@ type MessageEventContent = TextMessageEventContent | MediaMessageEventContent |
const MessageBody = ({ event }: EventContentProps) => {
const content = event.content as MessageEventContent
if (event.type === "m.sticker") {
content.msgtype = "m.image"
}
switch (content.msgtype) {
case "m.text":
case "m.emote":
@ -71,13 +75,19 @@ const MessageBody = ({ event }: EventContentProps) => {
}}/>
}
return content.body
case "m.image":
case "m.image": {
const style = calculateMediaSize(content.info?.w, content.info?.h)
if (content.url) {
return <img src={getMediaURL(content.url)} alt={content.body}/>
return <div className="media-container" style={style.container}>
<img style={style.media} src={getMediaURL(content.url)} alt={content.body}/>
</div>
} else if (content.file) {
return <img src={getMediaURL(content.file.url)} alt={content.body}/>
return <div className="media-container" style={style.container}>
<img style={style.media} src={getMediaURL(content.file.url)} alt={content.body}/>
</div>
}
}
}
return <code>{`{ "type": "${event.type}" }`}</code>
}

View file

@ -19,6 +19,7 @@
// 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"
import { calculateMediaSize } from "./mediasize.ts"
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/
@ -73,16 +74,14 @@ export const transformTags: NonNullable<sanitizeHtml.IOptions["transformTags"]>
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 && requestedHeight <= 48) {
attribs.style = `height: ${requestedHeight}px; width: auto; max-width: ${2 * requestedHeight}px;`
}
if (requestedHeight) {
attribs.style += "height: 100%;"
const style = calculateMediaSize(requestedWidth, requestedHeight)
if (style.media.aspectRatio) {
attribs.style = `width: ${style.container.width}; height: ${style.container.height};`
} else {
attribs.style = `height: 24px; width: auto; max-width: 48px;`
}
attribs.src = getMediaURL(src)!

61
web/src/util/mediasize.ts Normal file
View file

@ -0,0 +1,61 @@
// 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 { CSSProperties } from "react"
const imageContainerWidth = 320
const imageContainerHeight = 240
const imageContainerAspectRatio = imageContainerWidth / imageContainerHeight
export interface CalculatedMediaSize {
container: CSSProperties
media: CSSProperties
}
export function calculateMediaSize(width?: number, height?: number): CalculatedMediaSize {
if (!width || !height) {
return {
container: {
width: `${imageContainerWidth}px`,
height: `${imageContainerHeight}px`,
},
media: {},
}
}
const origWidth = width
const origHeight = height
if (width > imageContainerWidth || height > imageContainerHeight) {
const aspectRatio = width / height
if (aspectRatio > imageContainerAspectRatio) {
width = imageContainerWidth
height = imageContainerWidth / aspectRatio
} else if (aspectRatio < imageContainerAspectRatio) {
width = imageContainerHeight * aspectRatio
height = imageContainerHeight
} else {
width = imageContainerWidth
height = imageContainerHeight
}
}
return {
container: {
width: `${width}px`,
height: `${height}px`,
},
media: {
aspectRatio: `${origWidth} / ${origHeight}`,
},
}
}