mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/timeline: add support for other file types
This commit is contained in:
parent
11aa2eabf1
commit
6c55f1654c
6 changed files with 125 additions and 41 deletions
|
@ -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},
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@ function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps
|
|||
}
|
||||
switch (evt.type) {
|
||||
case "m.room.message":
|
||||
case "m.sticker":
|
||||
if (evt.redacted_by) {
|
||||
return RedactedBody
|
||||
}
|
||||
|
@ -56,11 +55,16 @@ function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps
|
|||
case "m.file":
|
||||
return MediaMessageBody
|
||||
case "m.location":
|
||||
// return LocationMessageBody
|
||||
// fallthrough
|
||||
// return LocationMessageBody
|
||||
// fallthrough
|
||||
default:
|
||||
return UnknownMessageBody
|
||||
}
|
||||
case "m.sticker":
|
||||
if (evt.redacted_by) {
|
||||
return RedactedBody
|
||||
}
|
||||
return MediaMessageBody
|
||||
case "m.room.encrypted":
|
||||
if (evt.redacted_by) {
|
||||
return RedactedBody
|
||||
|
|
|
@ -13,14 +13,15 @@
|
|||
//
|
||||
// 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 { 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<HTMLDivElement>) => {
|
||||
if ((evt.target as HTMLElement).closest("span[data-mx-spoiler]")?.classList.toggle("spoiler-revealed")) {
|
||||
|
@ -43,26 +44,74 @@ export const TextMessageBody = ({ event }: EventContentProps) => {
|
|||
return <div className="message-text plaintext-body">{content.body}</div>
|
||||
}
|
||||
|
||||
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 [<img
|
||||
loading="lazy"
|
||||
style={style.media}
|
||||
src={mediaURL}
|
||||
alt={content.filename ?? content.body}
|
||||
onClick={use(LightboxContext)}
|
||||
/>, "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<HTMLVideoElement> | undefined
|
||||
let onMouseOut: React.MouseEventHandler<HTMLVideoElement> | undefined
|
||||
if (!autoplay && !controls) {
|
||||
onMouseOver = (event: React.MouseEvent<HTMLVideoElement>) => event.currentTarget.play()
|
||||
onMouseOut = (event: React.MouseEvent<HTMLVideoElement>) => {
|
||||
event.currentTarget.pause()
|
||||
event.currentTarget.currentTime = 0
|
||||
}
|
||||
}
|
||||
return [<video
|
||||
autoPlay={autoplay}
|
||||
controls={controls}
|
||||
loop={loop}
|
||||
poster={thumbnailURL}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
preload="none"
|
||||
>
|
||||
<source src={mediaURL} type={content.info?.mimetype} />
|
||||
</video>, "video-container", {}]
|
||||
} else if (content.msgtype === "m.audio") {
|
||||
return [<audio controls src={mediaURL} preload="none"/>, "audio-container", {}]
|
||||
} else if (content.msgtype === "m.file") {
|
||||
return [
|
||||
<>
|
||||
<a
|
||||
href={mediaURL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
download={content.filename ?? content.body}
|
||||
><DownloadIcon height={32} width={32}/> {content.filename ?? content.body}</a>
|
||||
</>,
|
||||
"file-container",
|
||||
{},
|
||||
]
|
||||
}
|
||||
return [null, "unknown-container", {}]
|
||||
}
|
||||
|
||||
export const MediaMessageBody = ({ event, room }: EventContentProps) => {
|
||||
const content = event.content as MediaMessageEventContent
|
||||
if (event.type === "m.sticker") {
|
||||
content.msgtype = "m.image"
|
||||
}
|
||||
const openLightbox = use(LightboxContext)
|
||||
const style = calculateMediaSize(content.info?.w, content.info?.h)
|
||||
let caption = null
|
||||
if (content.body && content.filename && content.body !== content.filename) {
|
||||
caption = <TextMessageBody event={event} room={room} />
|
||||
}
|
||||
const [mediaContent, containerClass, containerStyle] = useMediaContent(content, event.type)
|
||||
return <>
|
||||
<div className="media-container" style={style.container}>
|
||||
<img
|
||||
loading="lazy"
|
||||
style={style.media}
|
||||
src={getMediaURL(content.url ?? content.file?.url)}
|
||||
alt={content.filename ?? content.body}
|
||||
onClick={openLightbox}
|
||||
/>
|
||||
<div className={`media-container ${containerClass}`} style={containerStyle}>
|
||||
{mediaContent}
|
||||
</div>
|
||||
{caption}
|
||||
</>
|
||||
|
|
Loading…
Add table
Reference in a new issue