Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add_msc4095_displaying.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add support for rendering bundled urls per MSC4095
30 changes: 26 additions & 4 deletions src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { memo, useMemo, useCallback } from 'react';
import { MsgType } from '$types/matrix-sdk';
import { IPreviewUrlResponse, MsgType } from '$types/matrix-sdk';
import { testMatrixTo } from '$plugins/matrix-to';
import { useSetting } from '$state/hooks/settings';
import { settingsAtom, CaptionPosition } from '$state/settings';
Expand Down Expand Up @@ -42,6 +42,7 @@ type RenderMessageContentProps = {
edited?: boolean;
getContent: <T>() => T;
mediaAutoLoad?: boolean;
bundledPreview?: boolean;
urlPreview?: boolean;
clientUrlPreview?: boolean;
highlightRegex?: RegExp;
Expand All @@ -68,6 +69,7 @@ function RenderMessageContentInternal({
edited,
getContent,
mediaAutoLoad,
bundledPreview,
urlPreview,
clientUrlPreview,
highlightRegex,
Expand Down Expand Up @@ -112,18 +114,17 @@ function RenderMessageContentInternal({

const mediaLinks = analyzed.filter((item) => item.type !== null);
const toRender = mediaLinks.length > 0 ? mediaLinks : [analyzed[0]];

return (
<UrlPreviewHolder>
{toRender.map(({ url, type }) => {
if (type) {
return <UrlPreviewCard key={url} url={url} ts={ts} mediaType={type} />;
return <UrlPreviewCard urlPreview key={url} url={url} ts={ts} mediaType={type} />;
}
if (clientUrlPreview && youtubeUrl(url)) {
return <ClientPreview url={url} />;
}
if (urlPreview) {
return <UrlPreviewCard key={url} url={url} ts={ts} mediaType={type} />;
return <UrlPreviewCard urlPreview key={url} url={url} ts={ts} mediaType={type} />;
}
return null;
})}
Expand All @@ -132,7 +133,23 @@ function RenderMessageContentInternal({
},
[ts, clientUrlPreview, urlPreview]
);
const renderBundledPreviews = useCallback(
(bundles: IPreviewUrlResponse[]) => (
<UrlPreviewHolder>
{bundles.map((bundle) => (
<UrlPreviewCard
urlPreview={urlPreview === true}
key={bundle['og:url']}
url={bundle['og:url']}
bundle={bundle}
/>
))}
</UrlPreviewHolder>
),
[urlPreview]
);
const messageUrlsPreview = urlPreview ? renderUrlsPreview : undefined;
const messageBundlePreview = bundledPreview ? renderBundledPreviews : undefined;

const renderCaption = () => {
const hasCaption = content.body && content.body.trim().length > 0;
Expand All @@ -146,6 +163,7 @@ function RenderMessageContentInternal({
content={content}
renderBody={renderBody}
renderUrlsPreview={messageUrlsPreview}
renderBundledPreviews={messageBundlePreview}
/>
);
return (
Expand All @@ -165,6 +183,7 @@ function RenderMessageContentInternal({
content={content}
renderBody={renderBody}
renderUrlsPreview={messageUrlsPreview}
renderBundledPreviews={messageBundlePreview}
/>
</Box>
);
Expand Down Expand Up @@ -227,6 +246,7 @@ function RenderMessageContentInternal({
content={content}
renderBody={renderBody}
renderUrlsPreview={messageUrlsPreview}
renderBundledPreviews={messageBundlePreview}
/>
);
}
Expand All @@ -248,6 +268,7 @@ function RenderMessageContentInternal({
content={content}
renderBody={renderBody}
renderUrlsPreview={messageUrlsPreview}
renderBundledPreviews={messageBundlePreview}
/>
);
}
Expand All @@ -259,6 +280,7 @@ function RenderMessageContentInternal({
content={content}
renderBody={renderBody}
renderUrlsPreview={messageUrlsPreview}
renderBundledPreviews={messageBundlePreview}
/>
);
}
Expand Down
72 changes: 59 additions & 13 deletions src/app/components/message/MsgTypeRenderers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CSSProperties, ReactNode, useMemo } from 'react';
import { Box, Chip, Icon, Icons, Text, toRem } from 'folds';
import { IContent } from '$types/matrix-sdk';
import { IContent, IPreviewUrlResponse } from '$types/matrix-sdk';
import { JUMBO_EMOJI_REG, URL_REG } from '$utils/regex';
import { trimReplyFromBody } from '$utils/room';
import {
Expand Down Expand Up @@ -82,9 +82,17 @@ type MTextProps = {
content: Record<string, unknown>;
renderBody: (props: RenderBodyProps) => ReactNode;
renderUrlsPreview?: (urls: string[]) => ReactNode;
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
style?: CSSProperties;
};
export function MText({ edited, content, renderBody, renderUrlsPreview, style }: MTextProps) {
export function MText({
edited,
content,
renderBody,
renderUrlsPreview,
renderBundledPreviews,
style,
}: MTextProps) {
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');

const body = typeof content.body === 'string' ? content.body : '';
Expand Down Expand Up @@ -139,8 +147,13 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:

if (!body && !customBody) return <BrokenContent body={customBody ?? body} />;

const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
let bundleContent: object[] | undefined;
const urlsMatch = trimmedBody.match(URL_REG);
let urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
bundleContent = content['com.beeper.linkpreviews'] as object[];
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));
if (renderUrlsPreview && bundleContent)
urls = bundleContent.map((bundle) => (bundle as any).matched_url);

if ((content['com.beeper.per_message_profile'] as PerMessageProfileBeeperFormat)?.has_fallback) {
// unwrap per-message profile fallback if present
Expand All @@ -167,7 +180,11 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
customBody: unwrappedForwardedContent,
})}
{edited && <MessageEditedContent />}
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
(renderBundledPreviews &&
bundleContent &&
bundleContent.length > 0 &&
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
</MessageTextBody>
);
}
Expand All @@ -185,7 +202,11 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
})}
{edited && <MessageEditedContent />}
</MessageTextBody>
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
(renderBundledPreviews &&
bundleContent &&
bundleContent.length > 0 &&
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
</>
);
}
Expand All @@ -196,13 +217,15 @@ type MEmoteProps = {
content: Record<string, unknown>;
renderBody: (props: RenderBodyProps) => ReactNode;
renderUrlsPreview?: (urls: string[]) => ReactNode;
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
};
export function MEmote({
displayName,
edited,
content,
renderBody,
renderUrlsPreview,
renderBundledPreviews,
}: MEmoteProps) {
const { body, formatted_body: customBody } = content;
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');
Expand All @@ -211,10 +234,14 @@ export function MEmote({
return <BrokenContent body={typeof customBody === 'string' ? customBody : undefined} />;
}
const trimmedBody = trimReplyFromBody(body);
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
const isJumbo = JUMBO_EMOJI_REG.test(trimmedBody);

let bundleContent: object[] | undefined;
const urlsMatch = trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
bundleContent = content['com.beeper.linkpreviews'] as object[];
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));

return (
<>
<MessageTextBody
Expand All @@ -229,7 +256,11 @@ export function MEmote({
})}
{edited && <MessageEditedContent />}
</MessageTextBody>
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
(renderBundledPreviews &&
bundleContent &&
bundleContent.length > 0 &&
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
</>
);
}
Expand All @@ -239,19 +270,30 @@ type MNoticeProps = {
content: Record<string, unknown>;
renderBody: (props: RenderBodyProps) => ReactNode;
renderUrlsPreview?: (urls: string[]) => ReactNode;
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
};
export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNoticeProps) {
export function MNotice({
edited,
content,
renderBody,
renderUrlsPreview,
renderBundledPreviews,
}: MNoticeProps) {
const { body, formatted_body: customBody } = content;
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');

if (typeof body !== 'string') {
return <BrokenContent body={typeof customBody === 'string' ? customBody : undefined} />;
}
const trimmedBody = trimReplyFromBody(body);
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
const isJumbo = JUMBO_EMOJI_REG.test(trimmedBody);

let bundleContent: object[] | undefined;
const urlsMatch = trimmedBody.match(URL_REG);
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
bundleContent = content['com.beeper.linkpreviews'] as object[];
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));

return (
<>
<MessageTextBody
Expand All @@ -265,7 +307,11 @@ export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNot
})}
{edited && <MessageEditedContent />}
</MessageTextBody>
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
(renderBundledPreviews &&
bundleContent &&
bundleContent.length > 0 &&
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
</>
);
}
Expand Down
Loading
Loading