diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index 8edba98..3ba9e50 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -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%; } } diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 0e48fc5..7b5764d 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -29,8 +29,8 @@ export interface TimelineEventProps { function getBodyType(evt: DBEvent): React.FunctionComponent { switch (evt.type) { case "m.room.message": - return MessageBody case "m.sticker": + return MessageBody } return HiddenEvent } diff --git a/web/src/ui/timeline/content/MessageBody.tsx b/web/src/ui/timeline/content/MessageBody.tsx index a8fa411..3193339 100644 --- a/web/src/ui/timeline/content/MessageBody.tsx +++ b/web/src/ui/timeline/content/MessageBody.tsx @@ -13,12 +13,13 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - +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 {content.body}/ + return
+ {content.body}/ +
} else if (content.file) { - return {content.body}/ + return
+ {content.body}/ +
} } + } return {`{ "type": "${event.type}" }`} } diff --git a/web/src/util/html.ts b/web/src/util/html.ts index b67432a..6859446 100644 --- a/web/src/util/html.ts +++ b/web/src/util/html.ts @@ -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 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)! diff --git a/web/src/util/mediasize.ts b/web/src/util/mediasize.ts new file mode 100644 index 0000000..a19fd60 --- /dev/null +++ b/web/src/util/mediasize.ts @@ -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 . +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}`, + }, + } +}