diff --git a/pkg/hicli/sync.go b/pkg/hicli/sync.go index ccc9d008..f87f9ea8 100644 --- a/pkg/hicli/sync.go +++ b/pkg/hicli/sync.go @@ -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) diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 2228124e..0f4802e8 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -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 { diff --git a/web/src/ui/timeline/LinkPreviews.css b/web/src/ui/timeline/LinkPreviews.css new file mode 100644 index 00000000..89dd5f87 --- /dev/null +++ b/web/src/ui/timeline/LinkPreviews.css @@ -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; + } + } +} diff --git a/web/src/ui/timeline/LinkPreviews.tsx b/web/src/ui/timeline/LinkPreviews.tsx new file mode 100644 index 00000000..20effdb9 --- /dev/null +++ b/web/src/ui/timeline/LinkPreviews.tsx @@ -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 . +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
+ {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
+
+ {p["og:title"] ?? p["og:url"] ?? p.matched_url} +
+
{p["og:description"]}
+ {mediaURL &&
+ {p["og:title"]} +
} +
+ })} +
+} + +export default React.memo(LinkPreviews) diff --git a/web/src/ui/timeline/TimelineEvent.tsx b/web/src/ui/timeline/TimelineEvent.tsx index c79b63c9..9fc0c89f 100644 --- a/web/src/ui/timeline/TimelineEvent.tsx +++ b/web/src/ui/timeline/TimelineEvent.tsx @@ -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" @@ -196,6 +197,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu }: TimelineEventProps) => { /> : null} + {!evt.redacted_by ? : null} {evt.reactions ? : null}