Skip to content

Commit

Permalink
web/timeline: render MSC4095 URL previews
Browse files Browse the repository at this point in the history
This commit implements rending of MSC4095[1] bundled URL previews.

[1]: matrix-org/matrix-spec-proposals#4095

Signed-off-by: Sumner Evans <[email protected]>
  • Loading branch information
sumnerevans committed Dec 20, 2024
1 parent 800331f commit e04f039
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 0 deletions.
16 changes: 16 additions & 0 deletions pkg/hicli/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions web/src/api/types/mxtypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
35 changes: 35 additions & 0 deletions web/src/ui/timeline/LinkPreviews.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
div.link-previews {
display: flex;
flex-direction: row;
gap: 1rem;
overflow-x: scroll;

> div.link-preview {
margin: 0.5rem 0;
border-radius: 0.5rem;
background-color: var(--pill-background-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;
}
}
}
54 changes: 54 additions & 0 deletions web/src/ui/timeline/LinkPreviews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 from "react"
import { getEncryptedMediaURL, getMediaURL } from "@/api/media"
import { MemDBEvent, URLPreview } from "@/api/types"
import { calculateMediaSize } from "@/util/mediasize"
import "./LinkPreviews.css"

const LinkPreviews = ({ event }: { event: MemDBEvent }) => {
const previews = (event.content["com.beeper.linkpreviews"] ?? event.content["m.url_previews"]) as URLPreview[]
if (!previews) {
return null
}
return <div className="link-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 style = calculateMediaSize(p["og:image:width"], p["og:image:height"])
return <div className="link-preview" style={{ width: style.container.width }}>
<div className="title">
<a href={p.matched_url}><b>{p["og:title"] ?? p["og:url"] ?? p.matched_url}</b></a>
</div>
<div className="description">{p["og:description"]}</div>
{mediaURL && <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(LinkPreviews)
2 changes: 2 additions & 0 deletions web/src/ui/timeline/TimelineEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ClientContext from "../ClientContext.ts"
import MainScreenContext from "../MainScreenContext.ts"
import { ModalContext } from "../modal/Modal.tsx"
import { useRoomContext } from "../roomview/roomcontext.ts"
import LinkPreviews from "./LinkPreviews.tsx"
import ReadReceipts from "./ReadReceipts.tsx"
import { ReplyIDBody } from "./ReplyBody.tsx"
import { ContentErrorBoundary, HiddenEvent, getBodyType, isSmallEvent } from "./content"
Expand Down Expand Up @@ -196,6 +197,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => {
/> : null}
<ContentErrorBoundary>
<BodyType room={roomCtx.store} sender={memberEvt} event={evt}/>
{!evt.redacted_by ? <LinkPreviews event={evt}/> : null}
</ContentErrorBoundary>
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
</div>
Expand Down

0 comments on commit e04f039

Please sign in to comment.