From 6c55f1654c2bf78407500f6fa444dc28d26346a1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2024 18:27:11 +0300 Subject: [PATCH] web/timeline: add support for other file types --- web/eslint.config.js | 8 +- web/src/api/media.ts | 8 +- web/src/api/types/mxtypes.ts | 42 +++++++---- web/src/ui/timeline/TimelineEvent.css | 17 +++++ web/src/ui/timeline/TimelineEvent.tsx | 10 ++- web/src/ui/timeline/content/MessageBody.tsx | 81 +++++++++++++++++---- 6 files changed, 125 insertions(+), 41 deletions(-) diff --git a/web/eslint.config.js b/web/eslint.config.js index 81f1bdf..06e8b7f 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -37,14 +37,14 @@ export default tseslint.config( "named": true, "warnOnUnassignedImports": true, "pathGroups": [{ - "pattern": "@/**", - "group": "parent", - "position": "before", - }, { "pattern": "*.svg?react", "patternOptions": {"matchBase": true}, "group": "sibling", "position": "after", + }, { + "pattern": "@/**", + "group": "parent", + "position": "before", }, { "pattern": "*.css", "patternOptions": {"matchBase": true}, diff --git a/web/src/api/media.ts b/web/src/api/media.ts index 2a53896..0de06ad 100644 --- a/web/src/api/media.ts +++ b/web/src/api/media.ts @@ -18,7 +18,7 @@ import { UserID } from "./types" const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/ -export const getMediaURL = (mxc?: string): string | undefined => { +export const getMediaURL = (mxc?: string, encrypted: boolean = false): string | undefined => { if (!mxc) { return undefined } @@ -26,7 +26,11 @@ export const getMediaURL = (mxc?: string): string | undefined => { if (!match) { return undefined } - return `_gomuks/media/${match[1]}/${match[2]}` + return `_gomuks/media/${match[1]}/${match[2]}?encrypted=${encrypted}` +} + +export const getEncryptedMediaURL = (mxc?: string): string | undefined => { + return getMediaURL(mxc, true) } export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => { diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index b530bd7..2764f02 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -79,22 +79,32 @@ export interface MediaMessageEventContent extends BaseMessageEventContent { msgtype: "m.image" | "m.file" | "m.audio" | "m.video" filename?: string 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 - } + file?: EncryptedFile + info?: MediaInfo +} + +export interface EncryptedFile { + url: ContentURI + k: string + v: "v2" + ext: true + alg: "A256CTR" + key_ops: string[] + kty: "oct" +} + +export interface MediaInfo { + mimetype?: string + size?: number + w?: number + h?: number + duration?: number + thumbnail_url?: ContentURI + thumbnail_file?: EncryptedFile + thumbnail_info?: MediaInfo + + "fi.mau.hide_controls"?: boolean + "fi.mau.loop"?: boolean } export interface LocationMessageEventContent extends BaseMessageEventContent { diff --git a/web/src/ui/timeline/TimelineEvent.css b/web/src/ui/timeline/TimelineEvent.css index e543506..3216059 100644 --- a/web/src/ui/timeline/TimelineEvent.css +++ b/web/src/ui/timeline/TimelineEvent.css @@ -199,8 +199,25 @@ div.html-body { } div.media-container { + &.video-container { + width: 100%; + height: 240px; + } + + > a { + display: flex; + align-items: center; + text-decoration: none; + color: inherit; + } + > img { max-width: 100%; max-height: 100%; } + > video { + min-width: 320px; + max-width: 100%; + height: 100%; + } } diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index 0d23a6c..12ce124 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -41,7 +41,6 @@ function getBodyType(evt: MemDBEvent): React.FunctionComponent. -import { use, useMemo } from "react" +import { CSSProperties, use, useMemo } from "react" import sanitizeHtml from "sanitize-html" -import { getMediaURL } from "@/api/media.ts" -import type { MediaMessageEventContent, MessageEventContent } from "@/api/types" +import { getEncryptedMediaURL, getMediaURL } from "@/api/media.ts" +import type { EventType, MediaMessageEventContent, MessageEventContent } from "@/api/types" import { sanitizeHtmlParams } from "@/util/html.ts" import { calculateMediaSize } from "@/util/mediasize.ts" import { LightboxContext } from "../../Lightbox.tsx" import { EventContentProps } from "./props.ts" +import DownloadIcon from "@/icons/download.svg?react" const onClickHTML = (evt: React.MouseEvent) => { if ((evt.target as HTMLElement).closest("span[data-mx-spoiler]")?.classList.toggle("spoiler-revealed")) { @@ -43,26 +44,74 @@ export const TextMessageBody = ({ event }: EventContentProps) => { return
{content.body}
} +const useMediaContent = ( + content: MediaMessageEventContent, evtType: EventType, +): [React.ReactElement | null, string, CSSProperties] => { + const mediaURL = content.url ? getMediaURL(content.url) : getEncryptedMediaURL(content.file?.url) + const thumbnailURL = content.info?.thumbnail_url + ? getMediaURL(content.info.thumbnail_url) : getEncryptedMediaURL(content.info?.thumbnail_file?.url) + if (content.msgtype === "m.image" || evtType === "m.sticker") { + const style = calculateMediaSize(content.info?.w, content.info?.h) + return [{content.filename, "image-container", style.container] + } else if (content.msgtype === "m.video") { + const autoplay = false + const controls = !content.info?.["fi.mau.hide_controls"] + const loop = !!content.info?.["fi.mau.loop"] + let onMouseOver: React.MouseEventHandler | undefined + let onMouseOut: React.MouseEventHandler | undefined + if (!autoplay && !controls) { + onMouseOver = (event: React.MouseEvent) => event.currentTarget.play() + onMouseOut = (event: React.MouseEvent) => { + event.currentTarget.pause() + event.currentTarget.currentTime = 0 + } + } + return [, "video-container", {}] + } else if (content.msgtype === "m.audio") { + return [