Skip to content

Commit e78e97f

Browse files
authored
Add support for displaying bundled previews MSC4095 (#590)
<!-- Please read https://github.com/SableClient/Sable/blob/dev/CONTRIBUTING.md before submitting your pull request --> ### Description This PR wishes to display bundled previews, should they be offered, in order to align to MSC4095. Implements: matrix-org/matrix-spec-proposals#4095 Also i swear i didnt modify anything past line 75 in /src/app/components/url-preview/UrlPreviewCart.tsx i just added a param to UrlPreviewCard which pushed the length over the limit breaking the params into a table format, which then put the closing angle bracket right before the arguments which then made it no longer take one indentation which pushed the entire code to have one less indentation for prettier and if i am to change anything meaninglessly it just reverts it 😭 #### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). <!-- Write any explanation required here, but do not generate the explanation using AI!! You must prove you understand what the code in this PR does. --> A Jihn waswasa-ed the implementation to me.
2 parents 8d55215 + dd842d6 commit e78e97f

8 files changed

Lines changed: 314 additions & 211 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
Add support for rendering bundled urls per MSC4095

src/app/components/RenderMessageContent.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { memo, useMemo, useCallback } from 'react';
2-
import { MsgType } from '$types/matrix-sdk';
2+
import { IPreviewUrlResponse, MsgType } from '$types/matrix-sdk';
33
import { parseSettingsLink } from '$features/settings/settingsLink';
44
import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl';
55
import { testMatrixTo } from '$plugins/matrix-to';
@@ -44,6 +44,7 @@ type RenderMessageContentProps = {
4444
edited?: boolean;
4545
getContent: <T>() => T;
4646
mediaAutoLoad?: boolean;
47+
bundledPreview?: boolean;
4748
urlPreview?: boolean;
4849
clientUrlPreview?: boolean;
4950
highlightRegex?: RegExp;
@@ -70,6 +71,7 @@ function RenderMessageContentInternal({
7071
edited,
7172
getContent,
7273
mediaAutoLoad,
74+
bundledPreview,
7375
urlPreview,
7476
clientUrlPreview,
7577
highlightRegex,
@@ -117,18 +119,17 @@ function RenderMessageContentInternal({
117119

118120
const mediaLinks = analyzed.filter((item) => item.type !== null);
119121
const toRender = mediaLinks.length > 0 ? mediaLinks : [analyzed[0]];
120-
121122
return (
122123
<UrlPreviewHolder>
123124
{toRender.map(({ url, type }) => {
124125
if (type) {
125-
return <UrlPreviewCard key={url} url={url} ts={ts} mediaType={type} />;
126+
return <UrlPreviewCard urlPreview key={url} url={url} ts={ts} mediaType={type} />;
126127
}
127128
if (clientUrlPreview && youtubeUrl(url)) {
128129
return <ClientPreview url={url} />;
129130
}
130131
if (urlPreview) {
131-
return <UrlPreviewCard key={url} url={url} ts={ts} mediaType={type} />;
132+
return <UrlPreviewCard urlPreview key={url} url={url} ts={ts} mediaType={type} />;
132133
}
133134
return null;
134135
})}
@@ -137,7 +138,23 @@ function RenderMessageContentInternal({
137138
},
138139
[ts, clientUrlPreview, settingsLinkBaseUrl, urlPreview]
139140
);
141+
const renderBundledPreviews = useCallback(
142+
(bundles: IPreviewUrlResponse[]) => (
143+
<UrlPreviewHolder>
144+
{bundles.map((bundle) => (
145+
<UrlPreviewCard
146+
urlPreview={urlPreview === true}
147+
key={bundle['og:url']}
148+
url={bundle['og:url']}
149+
bundle={bundle}
150+
/>
151+
))}
152+
</UrlPreviewHolder>
153+
),
154+
[urlPreview]
155+
);
140156
const messageUrlsPreview = urlPreview ? renderUrlsPreview : undefined;
157+
const messageBundlePreview = bundledPreview ? renderBundledPreviews : undefined;
141158

142159
const renderCaption = () => {
143160
const hasCaption = content.body && content.body.trim().length > 0;
@@ -151,6 +168,7 @@ function RenderMessageContentInternal({
151168
content={content}
152169
renderBody={renderBody}
153170
renderUrlsPreview={messageUrlsPreview}
171+
renderBundledPreviews={messageBundlePreview}
154172
/>
155173
);
156174
return (
@@ -170,6 +188,7 @@ function RenderMessageContentInternal({
170188
content={content}
171189
renderBody={renderBody}
172190
renderUrlsPreview={messageUrlsPreview}
191+
renderBundledPreviews={messageBundlePreview}
173192
/>
174193
</Box>
175194
);
@@ -232,6 +251,7 @@ function RenderMessageContentInternal({
232251
content={content}
233252
renderBody={renderBody}
234253
renderUrlsPreview={messageUrlsPreview}
254+
renderBundledPreviews={messageBundlePreview}
235255
/>
236256
);
237257
}
@@ -253,6 +273,7 @@ function RenderMessageContentInternal({
253273
content={content}
254274
renderBody={renderBody}
255275
renderUrlsPreview={messageUrlsPreview}
276+
renderBundledPreviews={messageBundlePreview}
256277
/>
257278
);
258279
}
@@ -264,6 +285,7 @@ function RenderMessageContentInternal({
264285
content={content}
265286
renderBody={renderBody}
266287
renderUrlsPreview={messageUrlsPreview}
288+
renderBundledPreviews={messageBundlePreview}
267289
/>
268290
);
269291
}

src/app/components/message/MsgTypeRenderers.tsx

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { CSSProperties, ReactNode, useMemo } from 'react';
22
import { Box, Chip, Icon, Icons, Text, toRem } from 'folds';
3-
import { IContent } from '$types/matrix-sdk';
3+
import { IContent, IPreviewUrlResponse } from '$types/matrix-sdk';
44
import { JUMBO_EMOJI_REG, URL_REG } from '$utils/regex';
55
import { trimReplyFromBody } from '$utils/room';
66
import {
@@ -82,9 +82,17 @@ type MTextProps = {
8282
content: Record<string, unknown>;
8383
renderBody: (props: RenderBodyProps) => ReactNode;
8484
renderUrlsPreview?: (urls: string[]) => ReactNode;
85+
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
8586
style?: CSSProperties;
8687
};
87-
export function MText({ edited, content, renderBody, renderUrlsPreview, style }: MTextProps) {
88+
export function MText({
89+
edited,
90+
content,
91+
renderBody,
92+
renderUrlsPreview,
93+
renderBundledPreviews,
94+
style,
95+
}: MTextProps) {
8896
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');
8997

9098
const body = typeof content.body === 'string' ? content.body : '';
@@ -130,8 +138,13 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
130138

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

133-
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
134-
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
141+
let bundleContent: object[] | undefined;
142+
const urlsMatch = trimmedBody.match(URL_REG);
143+
let urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
144+
bundleContent = content['com.beeper.linkpreviews'] as object[];
145+
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));
146+
if (renderUrlsPreview && bundleContent)
147+
urls = bundleContent.map((bundle) => (bundle as any).matched_url);
135148

136149
if ((content['com.beeper.per_message_profile'] as PerMessageProfileBeeperFormat)?.has_fallback) {
137150
// unwrap per-message profile fallback if present
@@ -158,7 +171,11 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
158171
customBody: unwrappedForwardedContent,
159172
})}
160173
{edited && <MessageEditedContent />}
161-
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
174+
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
175+
(renderBundledPreviews &&
176+
bundleContent &&
177+
bundleContent.length > 0 &&
178+
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
162179
</MessageTextBody>
163180
);
164181
}
@@ -176,7 +193,11 @@ export function MText({ edited, content, renderBody, renderUrlsPreview, style }:
176193
})}
177194
{edited && <MessageEditedContent />}
178195
</MessageTextBody>
179-
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
196+
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
197+
(renderBundledPreviews &&
198+
bundleContent &&
199+
bundleContent.length > 0 &&
200+
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
180201
</>
181202
);
182203
}
@@ -187,13 +208,15 @@ type MEmoteProps = {
187208
content: Record<string, unknown>;
188209
renderBody: (props: RenderBodyProps) => ReactNode;
189210
renderUrlsPreview?: (urls: string[]) => ReactNode;
211+
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
190212
};
191213
export function MEmote({
192214
displayName,
193215
edited,
194216
content,
195217
renderBody,
196218
renderUrlsPreview,
219+
renderBundledPreviews,
197220
}: MEmoteProps) {
198221
const { body, formatted_body: customBody } = content;
199222
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');
@@ -202,10 +225,14 @@ export function MEmote({
202225
return <BrokenContent body={typeof customBody === 'string' ? customBody : undefined} />;
203226
}
204227
const trimmedBody = trimReplyFromBody(body);
205-
const urlsMatch = renderUrlsPreview && trimmedBody.match(URL_REG);
206-
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
207228
const isJumbo = JUMBO_EMOJI_REG.test(trimmedBody);
208229

230+
let bundleContent: object[] | undefined;
231+
const urlsMatch = trimmedBody.match(URL_REG);
232+
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
233+
bundleContent = content['com.beeper.linkpreviews'] as object[];
234+
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));
235+
209236
return (
210237
<>
211238
<MessageTextBody
@@ -220,7 +247,11 @@ export function MEmote({
220247
})}
221248
{edited && <MessageEditedContent />}
222249
</MessageTextBody>
223-
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
250+
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
251+
(renderBundledPreviews &&
252+
bundleContent &&
253+
bundleContent.length > 0 &&
254+
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
224255
</>
225256
);
226257
}
@@ -230,19 +261,30 @@ type MNoticeProps = {
230261
content: Record<string, unknown>;
231262
renderBody: (props: RenderBodyProps) => ReactNode;
232263
renderUrlsPreview?: (urls: string[]) => ReactNode;
264+
renderBundledPreviews?: (bundles: IPreviewUrlResponse[]) => ReactNode;
233265
};
234-
export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNoticeProps) {
266+
export function MNotice({
267+
edited,
268+
content,
269+
renderBody,
270+
renderUrlsPreview,
271+
renderBundledPreviews,
272+
}: MNoticeProps) {
235273
const { body, formatted_body: customBody } = content;
236274
const [jumboEmojiSize] = useSetting(settingsAtom, 'jumboEmojiSize');
237275

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

282+
let bundleContent: object[] | undefined;
283+
const urlsMatch = trimmedBody.match(URL_REG);
284+
const urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
285+
bundleContent = content['com.beeper.linkpreviews'] as object[];
286+
bundleContent = bundleContent?.filter((bundle) => !!urls?.includes((bundle as any).matched_url));
287+
246288
return (
247289
<>
248290
<MessageTextBody
@@ -256,7 +298,11 @@ export function MNotice({ edited, content, renderBody, renderUrlsPreview }: MNot
256298
})}
257299
{edited && <MessageEditedContent />}
258300
</MessageTextBody>
259-
{renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)}
301+
{(renderUrlsPreview && urls && urls.length > 0 && renderUrlsPreview(urls)) ||
302+
(renderBundledPreviews &&
303+
bundleContent &&
304+
bundleContent.length > 0 &&
305+
renderBundledPreviews(bundleContent as IPreviewUrlResponse[]))}
260306
</>
261307
);
262308
}

0 commit comments

Comments
 (0)