forked from Mirrors/gomuks
web/timeline: render MSC4095 URL previews
This commit implements rending of MSC4095[1] bundled URL previews and includes a preference for disabling rendering of the previews. [1]: https://github.com/matrix-org/matrix-spec-proposals/pull/4095 Signed-off-by: Sumner Evans <me@sumnerevans.com>
This commit is contained in:
parent
1ff9ba241a
commit
4c4744eba8
6 changed files with 174 additions and 0 deletions
|
@ -411,6 +411,22 @@ func (h *HiClient) cacheMedia(ctx context.Context, evt *event.Event, rowID datab
|
|||
} else if content.GetInfo().ThumbnailURL != "" {
|
||||
h.addMediaCache(ctx, rowID, content.Info.ThumbnailURL, nil, content.Info.ThumbnailInfo, "")
|
||||
}
|
||||
|
||||
for _, image := range content.BeeperGalleryImages {
|
||||
h.cacheMedia(ctx, &event.Event{
|
||||
Type: event.EventMessage,
|
||||
Content: event.Content{Parsed: image},
|
||||
}, rowID)
|
||||
}
|
||||
|
||||
for _, preview := range content.BeeperLinkPreviews {
|
||||
info := &event.FileInfo{MimeType: preview.ImageType}
|
||||
if preview.ImageEncryption != nil {
|
||||
h.addMediaCache(ctx, rowID, preview.ImageEncryption.URL, preview.ImageEncryption, info, "")
|
||||
} else if preview.ImageURL != "" {
|
||||
h.addMediaCache(ctx, rowID, preview.ImageURL, nil, info, "")
|
||||
}
|
||||
}
|
||||
case event.StateRoomAvatar:
|
||||
_ = evt.Content.ParseRaw(evt.Type)
|
||||
content, ok := evt.Content.Parsed.(*event.RoomAvatarEventContent)
|
||||
|
|
|
@ -140,6 +140,19 @@ export interface ContentWarning {
|
|||
description?: string
|
||||
}
|
||||
|
||||
export interface URLPreview {
|
||||
matched_url: string
|
||||
"beeper:image:encryption"?: EncryptedFile
|
||||
"matrix:image:size": number
|
||||
"og:image"?: ContentURI
|
||||
"og:url": string
|
||||
"og:image:width"?: number
|
||||
"og:image:height"?: number
|
||||
"og:image:type"?: string
|
||||
"og:title"?: string
|
||||
"og:description"?: string
|
||||
}
|
||||
|
||||
export interface BaseMessageEventContent {
|
||||
msgtype: string
|
||||
body: string
|
||||
|
@ -150,6 +163,8 @@ export interface BaseMessageEventContent {
|
|||
"town.robin.msc3725.content_warning"?: ContentWarning
|
||||
"page.codeberg.everypizza.msc4193.spoiler"?: boolean
|
||||
"page.codeberg.everypizza.msc4193.spoiler.reason"?: string
|
||||
"m.url_previews"?: URLPreview[]
|
||||
"com.beeper.linkpreviews"?: URLPreview[]
|
||||
}
|
||||
|
||||
export interface TextMessageEventContent extends BaseMessageEventContent {
|
||||
|
|
|
@ -102,6 +102,12 @@ export const preferences = {
|
|||
allowedContexts: anyContext,
|
||||
defaultValue: true,
|
||||
}),
|
||||
render_url_previews: new Preference<boolean>({
|
||||
displayName: "Render URL previews",
|
||||
description: "Whether to render MSC4095 URL previews in the room timeline.",
|
||||
allowedContexts: anyContext,
|
||||
defaultValue: true,
|
||||
}),
|
||||
show_date_separators: new Preference<boolean>({
|
||||
displayName: "Show date separators",
|
||||
description: "Whether messages in different days should have a date separator between them in the room timeline.",
|
||||
|
|
|
@ -25,6 +25,7 @@ import { ModalContext } from "../modal/Modal.tsx"
|
|||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||
import ReadReceipts from "./ReadReceipts.tsx"
|
||||
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||
import URLPreviews from "./URLPreviews.tsx"
|
||||
import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content"
|
||||
import { EventFullMenu, EventHoverMenu, getModalStyleFromMouse } from "./menu"
|
||||
import ErrorIcon from "@/icons/error.svg?react"
|
||||
|
@ -196,6 +197,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
|
|||
/> : null}
|
||||
<ContentErrorBoundary>
|
||||
<BodyType room={roomCtx.store} sender={memberEvt} event={evt}/>
|
||||
<URLPreviews room={roomCtx.store} event={evt}/>
|
||||
</ContentErrorBoundary>
|
||||
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
||||
</div>
|
||||
|
|
48
web/src/ui/timeline/URLPreviews.css
Normal file
48
web/src/ui/timeline/URLPreviews.css
Normal file
|
@ -0,0 +1,48 @@
|
|||
div.url-previews {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
overflow-x: scroll;
|
||||
|
||||
> div.url-preview {
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--pill-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
div.title {
|
||||
margin: 0.5rem 0.5rem 0 0.5rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.description {
|
||||
margin: 0 0.5rem 0.5rem 0.5rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--semisecondary-text-color);
|
||||
}
|
||||
|
||||
> div.media-container {
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
|
||||
> div.media-container {
|
||||
border-radius: none;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
87
web/src/ui/timeline/URLPreviews.tsx
Normal file
87
web/src/ui/timeline/URLPreviews.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Sumner Evans
|
||||
//
|
||||
// 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 React, { use } from "react"
|
||||
import { getEncryptedMediaURL, getMediaURL } from "@/api/media"
|
||||
import { RoomStateStore, usePreference } from "@/api/statestore"
|
||||
import { MemDBEvent, URLPreview } from "@/api/types"
|
||||
import { ImageContainerSize, calculateMediaSize } from "@/util/mediasize"
|
||||
import ClientContext from "../ClientContext"
|
||||
import "./URLPreviews.css"
|
||||
|
||||
const URLPreviews = ({ event, room }: {
|
||||
room: RoomStateStore
|
||||
event: MemDBEvent
|
||||
}) => {
|
||||
const client = use(ClientContext)!
|
||||
const renderPreviews = usePreference(client.store, room, "render_url_previews")
|
||||
if (event.redacted_by || !renderPreviews) {
|
||||
return null
|
||||
}
|
||||
|
||||
const previews = (event.content["com.beeper.linkpreviews"] ?? event.content["m.url_previews"]) as URLPreview[]
|
||||
if (!previews) {
|
||||
return null
|
||||
}
|
||||
return <div className="url-previews">
|
||||
{previews
|
||||
.filter(p => p["og:title"] || p["og:image"] || p["beeper:image:encryption"])
|
||||
.map(p => {
|
||||
const mediaURL = p["beeper:image:encryption"]
|
||||
? getEncryptedMediaURL(p["beeper:image:encryption"].url)
|
||||
: getMediaURL(p["og:image"])
|
||||
const aspectRatio = (p["og:image:width"] ?? 1) / (p["og:image:height"] ?? 1)
|
||||
let containerSize: ImageContainerSize | undefined
|
||||
let inline = false
|
||||
if (aspectRatio < 1.2) {
|
||||
containerSize = { width: 70, height: 70 }
|
||||
inline = true
|
||||
}
|
||||
const style = calculateMediaSize(p["og:image:width"], p["og:image:height"], containerSize)
|
||||
|
||||
const title = p["og:title"] ?? p["og:url"] ?? p.matched_url
|
||||
return <div
|
||||
className={inline ? "url-preview inline" : "url-preview"}
|
||||
style={inline ? {} : { width: style.container.width }}>
|
||||
{mediaURL && inline && <div className="media-container" style={style.container}>
|
||||
<img
|
||||
loading="lazy"
|
||||
style={style.media}
|
||||
src={mediaURL}
|
||||
alt={p["og:title"]}
|
||||
title={p["og:title"]}
|
||||
/>
|
||||
</div>}
|
||||
<div className="title-description">
|
||||
<div className="title">
|
||||
<a href={p.matched_url} title={title} target="_blank"><b>{title}</b></a>
|
||||
</div>
|
||||
<div className="description">{p["og:description"]}</div>
|
||||
</div>
|
||||
{mediaURL && !inline && <div className="media-container" style={style.container}>
|
||||
<img
|
||||
loading="lazy"
|
||||
style={style.media}
|
||||
src={mediaURL}
|
||||
alt={p["og:title"]}
|
||||
title={p["og:title"]}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default React.memo(URLPreviews)
|
Loading…
Add table
Reference in a new issue