Redesign link previews (#33061)
* Commit design update * Add figma links * Check in other changes * revert accidental change * Iterative update * linting n test fiddles * linting * Cleanup * update snaps * Move URL previews to new home * Fix paths * compress img * Add back all the stories * Improved rendering * Fixup * Update previews again * lint * update stories * Update snaps again * More screenshots * Also these * Update snaps * include site name * Update snaps again * Use a scale so the images don't go blur * update snaps again * Update snaps * remove mistaken playwright cfg * update pw snaps * update snap * update previews * Update with new designs * Update screenshots
@ -252,6 +252,7 @@ test.describe("Message url previews", () => {
|
||||
"og:title": "A simple site",
|
||||
"og:description": "And with a brief description",
|
||||
"og:image": mxc,
|
||||
"og:image:alt": "The riot logo",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 29 KiB |
@ -13,7 +13,6 @@ import { type Interaction as InteractionEvent } from "@matrix-org/analytics-even
|
||||
import { type PinUnpinAction } from "@matrix-org/analytics-events/types/typescript/PinUnpinAction";
|
||||
import { type RoomListSortingAlgorithmChanged } from "@matrix-org/analytics-events/types/typescript/RoomListSortingAlgorithmChanged";
|
||||
import { type UrlPreviewRendered } from "@matrix-org/analytics-events/types/typescript/UrlPreviewRendered";
|
||||
import { type UrlPreview } from "@element-hq/web-shared-components";
|
||||
|
||||
import PageType from "./PageTypes";
|
||||
import Views from "./Views";
|
||||
@ -151,7 +150,7 @@ export default class PosthogTrackers {
|
||||
* @param isEncrypted Whether the event (and effectively the room) was encrypted.
|
||||
* @param previews The previews generated from the event.
|
||||
*/
|
||||
public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: UrlPreview[]): void {
|
||||
public trackUrlPreview(eventId: string, isEncrypted: boolean, previews: { image?: unknown }[]): void {
|
||||
// Discount any previews that we have already tracked.
|
||||
if (this.previewedEventIds.get(eventId)) {
|
||||
return;
|
||||
|
||||
@ -34,8 +34,10 @@ export interface UrlPreviewGroupViewModelProps {
|
||||
}
|
||||
|
||||
export const MAX_PREVIEWS_WHEN_LIMITED = 2;
|
||||
export const PREVIEW_WIDTH = 100;
|
||||
export const PREVIEW_HEIGHT = 100;
|
||||
export const PREVIEW_WIDTH_PX = 478;
|
||||
export const PREVIEW_HEIGHT_PX = 200;
|
||||
export const MIN_PREVIEW_PX = 96;
|
||||
export const MIN_IMAGE_SIZE_BYTES = 8192;
|
||||
|
||||
export enum PreviewVisibility {
|
||||
/**
|
||||
@ -100,21 +102,26 @@ export class UrlPreviewGroupViewModel
|
||||
typeof response["og:description"] === "string" && response["og:description"].trim()
|
||||
? response["og:description"].trim()
|
||||
: undefined;
|
||||
let siteName =
|
||||
const siteName =
|
||||
typeof response["og:site_name"] === "string" && response["og:site_name"].trim()
|
||||
? response["og:site_name"].trim()
|
||||
: undefined;
|
||||
: new URL(link).hostname;
|
||||
|
||||
// If there is no title, use the description as the title.
|
||||
if (!title && description) {
|
||||
title = description;
|
||||
description = undefined;
|
||||
} else if (!title && siteName) {
|
||||
title = siteName;
|
||||
siteName = undefined;
|
||||
} else if (!title) {
|
||||
title = link;
|
||||
}
|
||||
|
||||
// If the description matches the site name, don't bother with a description.
|
||||
if (description && description.toLowerCase() === siteName.toLowerCase()) {
|
||||
description = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description: description && decode(description),
|
||||
@ -122,6 +129,50 @@ export class UrlPreviewGroupViewModel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the best possible author from an opengraph response.
|
||||
* @param response The opengraph response
|
||||
* @returns The author value, or undefined if no valid author could be found.
|
||||
*/
|
||||
private static getAuthorFromResponse(response: IPreviewUrlResponse): UrlPreview["author"] {
|
||||
let calculatedAuthor: string | undefined;
|
||||
if (response["og:type"] === "article") {
|
||||
if (typeof response["article:author"] === "string" && response["article:author"]) {
|
||||
calculatedAuthor = response["article:author"];
|
||||
}
|
||||
// Otherwise fall through to check the profile.
|
||||
}
|
||||
if (typeof response["profile:username"] === "string" && response["profile:username"]) {
|
||||
calculatedAuthor = response["profile:username"];
|
||||
}
|
||||
if (calculatedAuthor && URL.canParse(calculatedAuthor)) {
|
||||
// Some sites return URLs as authors which doesn't look good in Element, so discard it.
|
||||
return;
|
||||
}
|
||||
return calculatedAuthor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate whether the provided image from the preview response is an full size preview or
|
||||
* a site icon.
|
||||
* @returns `true` if the image should be used as a preview, otherwise `false`
|
||||
*/
|
||||
private static isImagePreview(width?: number, height?: number, bytes?: number): boolean {
|
||||
// We can't currently distinguish from a preview image and a favicon. Neither OpenGraph nor Matrix
|
||||
// have a clear distinction, so we're using a heuristic here to check the dimensions & size of the file and
|
||||
// deciding whether to render it as a full preview or icon.
|
||||
if (width && width < MIN_PREVIEW_PX) {
|
||||
return false;
|
||||
}
|
||||
if (height && height < MIN_PREVIEW_PX) {
|
||||
return false;
|
||||
}
|
||||
if (bytes && bytes < MIN_IMAGE_SIZE_BYTES) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an anchor element can be rendered into a preview.
|
||||
* If it can, return the value of `href`
|
||||
@ -278,6 +329,7 @@ export class UrlPreviewGroupViewModel
|
||||
}
|
||||
|
||||
const { title, description, siteName } = UrlPreviewGroupViewModel.getBaseMetadataFromResponse(preview, link);
|
||||
const author = UrlPreviewGroupViewModel.getAuthorFromResponse(preview);
|
||||
const hasImage = preview["og:image"] && typeof preview?.["og:image"] === "string";
|
||||
// Ensure we have something relevant to render.
|
||||
// The title must not just be the link, or we must have an image.
|
||||
@ -285,31 +337,46 @@ export class UrlPreviewGroupViewModel
|
||||
return null;
|
||||
}
|
||||
let image: UrlPreview["image"];
|
||||
let siteIcon: string | undefined;
|
||||
if (typeof preview["og:image"] === "string" && this.visibility > PreviewVisibility.MediaHidden) {
|
||||
const media = mediaFromMxc(preview["og:image"], this.client);
|
||||
const declaredHeight = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["og:image:height"]);
|
||||
const declaredWidth = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["og:image:width"]);
|
||||
const width = Math.min(declaredWidth ?? PREVIEW_WIDTH, PREVIEW_WIDTH);
|
||||
const height = thumbHeight(width, declaredHeight, PREVIEW_WIDTH, PREVIEW_WIDTH) ?? PREVIEW_WIDTH;
|
||||
const thumb = media.getThumbnailOfSourceHttp(PREVIEW_WIDTH, PREVIEW_HEIGHT, "scale");
|
||||
// No thumb, no preview.
|
||||
if (thumb) {
|
||||
image = {
|
||||
imageThumb: thumb,
|
||||
imageFull: media.srcHttp ?? thumb,
|
||||
width,
|
||||
height,
|
||||
fileSize: UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["matrix:image:size"]),
|
||||
};
|
||||
const imageSize = UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["matrix:image:size"]);
|
||||
const alt = typeof preview["og:image:alt"] === "string" ? preview["og:image:alt"] : undefined;
|
||||
|
||||
const isImagePreview = UrlPreviewGroupViewModel.isImagePreview(declaredWidth, declaredHeight, imageSize);
|
||||
if (isImagePreview) {
|
||||
const width = Math.min(declaredWidth ?? PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX);
|
||||
const height =
|
||||
thumbHeight(width, declaredHeight, PREVIEW_WIDTH_PX, PREVIEW_WIDTH_PX) ?? PREVIEW_WIDTH_PX;
|
||||
const thumb = media.getThumbnailOfSourceHttp(PREVIEW_WIDTH_PX, PREVIEW_HEIGHT_PX, "scale");
|
||||
const playable = !!preview["og:video"] || !!preview["og:video:type"] || !!preview["og:audio"];
|
||||
// No thumb, no preview.
|
||||
if (thumb) {
|
||||
image = {
|
||||
imageThumb: thumb,
|
||||
imageFull: media.srcHttp ?? thumb,
|
||||
width,
|
||||
height,
|
||||
fileSize: UrlPreviewGroupViewModel.getNumberFromOpenGraph(preview["matrix:image:size"]),
|
||||
alt,
|
||||
playable,
|
||||
};
|
||||
}
|
||||
} else if (media.srcHttp) {
|
||||
siteIcon = media.srcHttp;
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
link,
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
siteName,
|
||||
showTooltipOnLink: link !== title && PlatformPeg.get()?.needsUrlTooltips(),
|
||||
siteIcon,
|
||||
showTooltipOnLink: !!(link !== title && PlatformPeg.get()?.needsUrlTooltips()),
|
||||
image,
|
||||
} satisfies UrlPreview;
|
||||
this.previewCache.set(link, result);
|
||||
|
||||
@ -18,20 +18,10 @@ describe("PosthogTrackers", () => {
|
||||
const tracker = new PosthogTrackers();
|
||||
tracker.trackUrlPreview("$123456", false, [
|
||||
{
|
||||
title: "A preview",
|
||||
image: {
|
||||
imageThumb: "abc",
|
||||
imageFull: "abc",
|
||||
},
|
||||
link: "a-link",
|
||||
},
|
||||
]);
|
||||
tracker.trackUrlPreview("$123456", false, [
|
||||
{
|
||||
title: "A second preview",
|
||||
link: "a-link",
|
||||
image: {},
|
||||
},
|
||||
]);
|
||||
tracker.trackUrlPreview("$123456", false, [{}]);
|
||||
// Ignores subsequent calls.
|
||||
expect(PosthogAnalytics.instance.trackEvent).toHaveBeenCalledWith({
|
||||
eventName: "UrlPreviewRendered",
|
||||
|
||||
@ -125,6 +125,32 @@ describe("UrlPreviewGroupViewModel", () => {
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot()).toMatchSnapshot();
|
||||
});
|
||||
it.each<Partial<IPreviewUrlResponse>>([
|
||||
{ "matrix:image:size": 8191 },
|
||||
{ "og:image:width": 95 },
|
||||
{ "og:image:height": 95 },
|
||||
])("should preview a URL with a site icon", async (extraResp) => {
|
||||
const { vm, client } = getViewModel();
|
||||
client.getUrlPreview.mockResolvedValueOnce({
|
||||
"og:title": "This is an example!",
|
||||
"og:type": "document",
|
||||
"og:url": "https://example.org",
|
||||
"og:image": IMAGE_MXC,
|
||||
"og:image:height": 128,
|
||||
"og:image:width": 128,
|
||||
"matrix:image:size": 8193,
|
||||
...extraResp,
|
||||
});
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
client.mxcUrlToHttp.mockImplementation((url) => {
|
||||
expect(url).toEqual(IMAGE_MXC);
|
||||
return "https://example.org/image/src";
|
||||
});
|
||||
const msg = document.createElement("div");
|
||||
msg.innerHTML = '<a href="https://example.org">Test</a>';
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0].siteIcon).toBeTruthy();
|
||||
});
|
||||
it("should ignore media when mediaVisible is false", async () => {
|
||||
const { vm, client } = getViewModel({ mediaVisible: false, visible: true });
|
||||
client.getUrlPreview.mockResolvedValueOnce({
|
||||
@ -200,6 +226,41 @@ describe("UrlPreviewGroupViewModel", () => {
|
||||
expect(vm.getSnapshot()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("calculates author", () => {
|
||||
it("should use the profile:username if provided", async () => {
|
||||
const { vm, client } = getViewModel();
|
||||
client.getUrlPreview.mockResolvedValueOnce({ ...BASIC_PREVIEW_OGDATA, "profile:username": "my username" });
|
||||
const msg = document.createElement("div");
|
||||
msg.innerHTML = '<a href="https://example.org">Test</a>';
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0].author).toEqual("my username");
|
||||
});
|
||||
it("should use author if the og:type is an article", async () => {
|
||||
const { vm, client } = getViewModel();
|
||||
client.getUrlPreview.mockResolvedValueOnce({
|
||||
...BASIC_PREVIEW_OGDATA,
|
||||
"og:type": "article",
|
||||
"article:author": "my name",
|
||||
});
|
||||
const msg = document.createElement("div");
|
||||
msg.innerHTML = '<a href="https://example.org">Test</a>';
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0].author).toEqual("my name");
|
||||
});
|
||||
it("should NOT use author if the author is a URL", async () => {
|
||||
const { vm, client } = getViewModel();
|
||||
client.getUrlPreview.mockResolvedValueOnce({
|
||||
...BASIC_PREVIEW_OGDATA,
|
||||
"og:type": "article",
|
||||
"article:author": "https://junk.example.org/foo",
|
||||
});
|
||||
const msg = document.createElement("div");
|
||||
msg.innerHTML = '<a href="https://example.org">Test</a>';
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0].author).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ text: "", href: "", hasPreview: false },
|
||||
{ text: "test", href: "noprotocol.example.org", hasPreview: false },
|
||||
@ -232,7 +293,7 @@ describe("UrlPreviewGroupViewModel", () => {
|
||||
// API *may* return a string, so check we parse correctly.
|
||||
"og:image:height": "500" as unknown as number,
|
||||
"og:image:width": 500,
|
||||
"matrix:image:size": 1024,
|
||||
"matrix:image:size": 10000,
|
||||
"og:image": IMAGE_MXC,
|
||||
},
|
||||
])("handles different kinds of opengraph responses %s", async (og) => {
|
||||
@ -251,4 +312,25 @@ describe("UrlPreviewGroupViewModel", () => {
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each<string>(["og:video", "og:video:type", "og:audio"])("detects playable links via %s", async (property) => {
|
||||
const { vm, client } = getViewModel();
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
client.mxcUrlToHttp.mockImplementation((url, width) => {
|
||||
expect(url).toEqual(IMAGE_MXC);
|
||||
if (width) {
|
||||
return "https://example.org/image/thumb";
|
||||
}
|
||||
return "https://example.org/image/src";
|
||||
});
|
||||
client.getUrlPreview.mockResolvedValueOnce({
|
||||
...BASIC_PREVIEW_OGDATA,
|
||||
"og:image": IMAGE_MXC,
|
||||
[property]: "anything",
|
||||
});
|
||||
const msg = document.createElement("div");
|
||||
msg.innerHTML = `<a href="https://example.org">test</a>`;
|
||||
await vm.updateEventElement(msg);
|
||||
expect(vm.getSnapshot().previews[0].image?.playable).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,61 +2,73 @@
|
||||
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:description': 'A description',\\n 'og:title': ''\\n} 1`] = `
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "example.org",
|
||||
"title": "A description",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:site_name': 'Site name',\\n 'og:title': ''\\n} 1`] = `
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "Site name",
|
||||
"title": "Site name",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Basic title'\\n} 1`] = `
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "example.org",
|
||||
"title": "Basic title",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Cool blog',\\n 'og:site_name': 'Cool site'\\n} 1`] = `
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "Cool site",
|
||||
"title": "Cool blog",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Media test',\\n 'og:image:height': '500',\\n 'og:image:width': 500,\\n 'matrix:image:size': 1024,\\n 'og:image': 'mxc://example.org/abc'\\n} 1`] = `
|
||||
exports[`UrlPreviewGroupViewModel handles different kinds of opengraph responses {\\n 'og:url': 'https://example.org',\\n 'og:type': 'document',\\n 'og:title': 'Media test',\\n 'og:image:height': '500',\\n 'og:image:width': 500,\\n 'matrix:image:size': 10000,\\n 'og:image': 'mxc://example.org/abc'\\n} 1`] = `
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": {
|
||||
"fileSize": 1024,
|
||||
"height": 100,
|
||||
"alt": undefined,
|
||||
"fileSize": 10000,
|
||||
"height": 478,
|
||||
"imageFull": "https://example.org/image/src",
|
||||
"imageThumb": "https://example.org/image/thumb",
|
||||
"width": 100,
|
||||
"playable": false,
|
||||
"width": 478,
|
||||
},
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "example.org",
|
||||
"title": "Media test",
|
||||
}
|
||||
`;
|
||||
@ -67,10 +79,12 @@ exports[`UrlPreviewGroupViewModel should deduplicate multiple versions of the sa
|
||||
"overPreviewLimit": false,
|
||||
"previews": [
|
||||
{
|
||||
"author": undefined,
|
||||
"description": "This is a description",
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "Example.org",
|
||||
"title": "This is an example!",
|
||||
},
|
||||
@ -96,10 +110,12 @@ exports[`UrlPreviewGroupViewModel should handle being hidden and shown by the us
|
||||
"overPreviewLimit": false,
|
||||
"previews": [
|
||||
{
|
||||
"author": undefined,
|
||||
"description": "This is a description",
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "Example.org",
|
||||
"title": "This is an example!",
|
||||
},
|
||||
@ -135,11 +151,13 @@ exports[`UrlPreviewGroupViewModel should ignore media when mediaVisible is false
|
||||
"overPreviewLimit": false,
|
||||
"previews": [
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "example.org",
|
||||
"title": "This is an example!",
|
||||
},
|
||||
],
|
||||
@ -154,17 +172,21 @@ exports[`UrlPreviewGroupViewModel should preview a URL with media 1`] = `
|
||||
"overPreviewLimit": false,
|
||||
"previews": [
|
||||
{
|
||||
"author": undefined,
|
||||
"description": undefined,
|
||||
"image": {
|
||||
"alt": undefined,
|
||||
"fileSize": 10000,
|
||||
"height": 100,
|
||||
"height": 128,
|
||||
"imageFull": "https://example.org/image/src",
|
||||
"imageThumb": "https://example.org/image/thumb",
|
||||
"width": 100,
|
||||
"playable": false,
|
||||
"width": 128,
|
||||
},
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"siteName": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "example.org",
|
||||
"title": "This is an example!",
|
||||
},
|
||||
],
|
||||
@ -179,10 +201,12 @@ exports[`UrlPreviewGroupViewModel should preview a single valid URL 1`] = `
|
||||
"overPreviewLimit": false,
|
||||
"previews": [
|
||||
{
|
||||
"author": undefined,
|
||||
"description": "This is a description",
|
||||
"image": undefined,
|
||||
"link": "https://example.org",
|
||||
"showTooltipOnLink": undefined,
|
||||
"showTooltipOnLink": false,
|
||||
"siteIcon": undefined,
|
||||
"siteName": "Example.org",
|
||||
"title": "This is an example!",
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 168 KiB |
@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.thumbnail {
|
||||
/* Thumbnails are always limited to a maximum of 100px */
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
/* Ensure we don't stretch the image */
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--cpd-color-text-link-external);
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: inline flex;
|
||||
column-gap: var(--cpd-space-1x);
|
||||
border-inline-start: 2px solid var(--cpd-color-bg-subtle-primary);
|
||||
border-radius: 2px;
|
||||
color: var(--cpd-color-gray-900);
|
||||
|
||||
.wrapImageCaption {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: var(--cpd-space-2x);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.image,
|
||||
.caption {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin-inline-start: var(--cpd-space-4x);
|
||||
min-width: 0; /* Prevent blowout */
|
||||
}
|
||||
|
||||
.image {
|
||||
/* Clear default <button> styles */
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
/* Consistent image size */
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
flex: 1;
|
||||
overflow: hidden; /* cause it to wrap rather than clip */
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
display: inline-block;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: var(--cpd-space-1x) 0;
|
||||
|
||||
> a {
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: var(--cpd-space-1x) 0;
|
||||
word-wrap: break-word;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import imageFile from "../../../../static/element.png";
|
||||
import { LinkPreview } from "./LinkPreview";
|
||||
import { LinkedTextContext } from "../../../core/utils/LinkedText";
|
||||
|
||||
export default {
|
||||
title: "Event/UrlPreviewGroupView/LinkPreview",
|
||||
component: LinkPreview,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
onImageClick: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof LinkPreview>;
|
||||
|
||||
const Template: StoryFn<typeof LinkPreview> = (args) => (
|
||||
<LinkedTextContext.Provider value={{}}>
|
||||
<LinkPreview {...args} />
|
||||
</LinkedTextContext.Provider>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
link: "https://matrix.org",
|
||||
siteName: "Site name",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
},
|
||||
};
|
||||
|
||||
export const Title = Template.bind({});
|
||||
Title.args = {
|
||||
title: "A simple title",
|
||||
link: "https://matrix.org",
|
||||
};
|
||||
|
||||
export const TitleAndDescription = Template.bind({});
|
||||
TitleAndDescription.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description with a link to https://matrix.org",
|
||||
link: "https://matrix.org",
|
||||
};
|
||||
|
||||
export const WithTooltip = Template.bind({});
|
||||
WithTooltip.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
showTooltipOnLink: true,
|
||||
link: "https://matrix.org",
|
||||
};
|
||||
|
||||
export const WithVeryLongText = Template.bind({});
|
||||
WithVeryLongText.args = {
|
||||
title: "GitHub - element-hq/not-a-real-repo: A very very long PR title that should be rendered nicely",
|
||||
description:
|
||||
"This PR doesn't actually exist and neither does the repository. It might exist one day if we go into the business of making paradoxical repository names.",
|
||||
link: "https://matrix.org",
|
||||
siteName: "GitHub",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
},
|
||||
};
|
||||
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type MouseEventHandler, type JSX, useCallback, useMemo } from "react";
|
||||
import { Tooltip, Text } from "@vector-im/compound-web";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useI18n } from "../../../core/i18n/i18nContext";
|
||||
import styles from "./LinkPreview.module.css";
|
||||
import type { UrlPreview } from "../types";
|
||||
import { LinkedText } from "../../../core/utils/LinkedText";
|
||||
|
||||
export interface LinkPreviewActions {
|
||||
onImageClick: () => void;
|
||||
}
|
||||
|
||||
export type LinkPreviewProps = UrlPreview & LinkPreviewActions;
|
||||
|
||||
/**
|
||||
* LinkPreview renders a single preview component for a single link on an event. It is usually rendered as part of
|
||||
* a `UrlPreviewGroupView`.
|
||||
*/
|
||||
export function LinkPreview({ onImageClick, ...preview }: LinkPreviewProps): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
const tooltipCaption = useMemo(() => {
|
||||
if (preview.showTooltipOnLink) {
|
||||
return new URL(preview.link, window.location.href).toString();
|
||||
}
|
||||
return null;
|
||||
}, [preview.link, preview.showTooltipOnLink]);
|
||||
|
||||
const onImageClickHandler = useCallback<MouseEventHandler>(
|
||||
(ev) => {
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
|
||||
if (!preview.image?.imageFull) {
|
||||
return;
|
||||
}
|
||||
onImageClick();
|
||||
},
|
||||
[preview.image?.imageFull, onImageClick],
|
||||
);
|
||||
|
||||
let img: JSX.Element | undefined;
|
||||
// Don't render a button to show the image, just hide it outright
|
||||
if (preview.image?.imageThumb) {
|
||||
img = (
|
||||
<button
|
||||
aria-label={_t("timeline|url_preview|view_image")}
|
||||
className={styles.image}
|
||||
onClick={onImageClickHandler}
|
||||
>
|
||||
<img className={styles.thumbnail} src={preview.image.imageThumb} alt="" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const anchor = (
|
||||
<a className={styles.link} href={preview.link} target="_blank" rel="noreferrer noopener">
|
||||
{preview.title}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<div className={classNames(styles.container)}>
|
||||
<div className={styles.wrapImageCaption}>
|
||||
{img}
|
||||
<div className={styles.caption}>
|
||||
<Text type="body" size="md" className={styles.title}>
|
||||
{tooltipCaption ? <Tooltip label={tooltipCaption}>{anchor}</Tooltip> : anchor}
|
||||
{preview.siteName && (
|
||||
<Text as="span" size="md" weight="regular">
|
||||
{" - " + preview.siteName}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
{preview.description && (
|
||||
<LinkedText className={styles.description}>{preview.description}</LinkedText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`LinkPreview > renders a preview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50"
|
||||
>
|
||||
- Site name
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LinkPreview > renders a preview with just a title 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LinkPreview > renders a preview with just a title and description 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description with a link to
|
||||
<a
|
||||
data-linkified="true"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://matrix.org
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -1,492 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UrlPreviewGroupView > renders a single preview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 20px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders multiple previews 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
One
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A regular square image.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/tallImage.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Two
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
This one has a taller image which should crop nicely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Three
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
One more description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Collapse
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 20px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders multiple previews which are hidden 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show 9 other previews
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 20px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders with a compact view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup UrlPreviewGroupView-module_compactLayout"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
One
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A regular square image.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/tallImage.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Two
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
This one has a taller image which should crop nicely.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_wrapImageCaption"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_image"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="LinkPreview-module_thumbnail"
|
||||
src="/static/element.png"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_caption"
|
||||
>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkPreview-module_title"
|
||||
>
|
||||
<a
|
||||
class="LinkPreview-module_link"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Three
|
||||
</a>
|
||||
</p>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
One more description
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Collapse
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 20px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -224,6 +224,7 @@
|
||||
"message_timestamp_sent_at": "Sent at: %(dateTime)s",
|
||||
"url_preview": {
|
||||
"close": "Close preview",
|
||||
"open_link": "Open link",
|
||||
"show_n_more": {
|
||||
"one": "Show %(count)s other preview",
|
||||
"other": "Show %(count)s other previews"
|
||||
|
||||
@ -12,7 +12,6 @@ export * from "./audio/SeekBar";
|
||||
export * from "./core/AvatarWithDetails";
|
||||
export * from "./room/composer/Banner";
|
||||
export * from "./crypto/SasEmoji";
|
||||
export * from "./event-tiles/UrlPreviewGroupView";
|
||||
export * from "./room/timeline/ReadMarker";
|
||||
export * from "./room/timeline/event-tile/body/EventContentBodyView";
|
||||
export * from "./room/timeline/event-tile/body/RedactedBodyView";
|
||||
@ -42,6 +41,7 @@ export * from "./room/timeline/event-tile/reactions/ReactionsRow";
|
||||
export * from "./room/timeline/event-tile/reactions/ReactionsRowButton";
|
||||
export * from "./room/timeline/event-tile/reactions/ReactionsRowButtonTooltip";
|
||||
export * from "./room/timeline/event-tile/timestamp/MessageTimestampView";
|
||||
export * from "./room/timeline/event-tile/UrlPreviewGroupView";
|
||||
export * from "./core/rich-list/RichItem";
|
||||
export * from "./core/rich-list/RichList";
|
||||
export * from "./room-list/RoomListHeaderView";
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
button.preview {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border: none;
|
||||
padding: 0;
|
||||
> img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.playButton[data-kind="primary"] {
|
||||
padding: 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: auto;
|
||||
background: var(--cpd-color-text-on-solid-primary);
|
||||
> svg {
|
||||
margin: auto;
|
||||
border-radius: 50px;
|
||||
color: var(--cpd-color-icon-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 478px;
|
||||
display: flex;
|
||||
border: 1px solid var(--cpd-color-bg-subtle-secondary);
|
||||
border-radius: 12px; /* Get radius from cpd */
|
||||
flex-direction: column;
|
||||
color: var(--cpd-color-gray-900);
|
||||
overflow: clip;
|
||||
|
||||
background: var(--cpd-color-bg-subtle-secondary);
|
||||
|
||||
&.inline {
|
||||
flex-direction: row;
|
||||
gap: var(--cpd-space-4x);
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.siteAvatar {
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.siteName {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textContent {
|
||||
padding: var(--cpd-space-3x) var(--cpd-space-4x);
|
||||
&.inline {
|
||||
padding: 0;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-1x);
|
||||
}
|
||||
|
||||
.caption {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
min-width: 0; /* Prevent blowout */
|
||||
}
|
||||
.caption {
|
||||
flex: 1;
|
||||
overflow: hidden; /* cause it to wrap rather than clip */
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
display: inline-block;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
margin: var(--cpd-space-1x) 0;
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
color: var(--cpd-color-text-primary);
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--cpd-font-size-body-lg);
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.siteName {
|
||||
margin-top: var(--cpd-space-1x);
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
gap: var(--cpd-space-1-5x);
|
||||
> * {
|
||||
/* Center everything */
|
||||
margin: auto 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import { LinkPreview } from "./LinkPreview";
|
||||
import { LinkedTextContext } from "../../../../../core/utils/LinkedText";
|
||||
import imageFile from "../../../../../../static/element.png";
|
||||
import imageFileWide from "../../../../../../static/wideImage.png";
|
||||
|
||||
export default {
|
||||
title: "Event/UrlPreviewGroupView/LinkPreview",
|
||||
component: LinkPreview,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
onImageClick: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
siteName: {
|
||||
control: "text",
|
||||
},
|
||||
author: {
|
||||
control: "text",
|
||||
},
|
||||
siteIcon: { control: { type: "file", accept: ".png" } },
|
||||
image: {},
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/sI9A2kV2K4xeiyqJsL7Ey3/Link-Previews?node-id=87-7920",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof LinkPreview>;
|
||||
|
||||
const Template: StoryFn<typeof LinkPreview> = (args) => (
|
||||
<LinkedTextContext.Provider value={{}}>
|
||||
<LinkPreview {...args} />
|
||||
</LinkedTextContext.Provider>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
link: "https://matrix.org",
|
||||
siteName: "Site name",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
alt: "Element logo",
|
||||
playable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Title = Template.bind({});
|
||||
Title.args = {
|
||||
title: "A simple title",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
};
|
||||
|
||||
export const TitleAndDescription = Template.bind({});
|
||||
TitleAndDescription.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description with a link to https://matrix.org",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
};
|
||||
export const WithSiteIcon = Template.bind({});
|
||||
WithSiteIcon.args = {
|
||||
title: "A simple title",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
siteIcon: imageFile,
|
||||
};
|
||||
|
||||
export const WithSiteIconAndDescription = Template.bind({});
|
||||
WithSiteIconAndDescription.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description with a link to https://matrix.org",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
siteIcon: imageFile,
|
||||
};
|
||||
|
||||
export const WithTooltip = Template.bind({});
|
||||
WithTooltip.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
showTooltipOnLink: true,
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
};
|
||||
|
||||
export const Article = Template.bind({});
|
||||
Article.args = {
|
||||
title: "A linked article",
|
||||
description:
|
||||
"This is a basic description returned from the linked source, usually with a word or two about what the link contains.",
|
||||
link: "https://matrix.org",
|
||||
siteName: "blog.example.org",
|
||||
image: {
|
||||
imageThumb: imageFileWide,
|
||||
imageFull: imageFileWide,
|
||||
alt: "A dog",
|
||||
playable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const Video = Template.bind({});
|
||||
Video.args = {
|
||||
title: "A linked video",
|
||||
description:
|
||||
"This is a link to a video. You cannot play the video inline yet, but you can click the play button to open the link",
|
||||
link: "https://matrix.org",
|
||||
siteName: "blog.example.org",
|
||||
image: {
|
||||
imageThumb: imageFileWide,
|
||||
imageFull: imageFileWide,
|
||||
alt: "A dog",
|
||||
playable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Social = Template.bind({});
|
||||
Social.args = {
|
||||
description: "Sending a small message",
|
||||
link: "https://matrix.org",
|
||||
siteName: "socialsite.example.org",
|
||||
title: "Test user (@test)",
|
||||
author: "@test",
|
||||
};
|
||||
|
||||
export const SocialWithImage = Template.bind({});
|
||||
SocialWithImage.args = {
|
||||
description: "Sending a message with an attached image.",
|
||||
title: "Test user (@test)",
|
||||
link: "https://matrix.org",
|
||||
siteName: "socialsite.example.org",
|
||||
author: "@test",
|
||||
image: {
|
||||
imageThumb: imageFileWide,
|
||||
imageFull: imageFileWide,
|
||||
alt: "A dog",
|
||||
playable: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithVeryLongText = Template.bind({});
|
||||
WithVeryLongText.args = {
|
||||
title: "GitHub - element-hq/not-a-real-repo: A very very long PR title that should be rendered nicely",
|
||||
description:
|
||||
"This PR doesn't actually exist and neither does the repository. It might exist one day if we go into the business of making paradoxical repository names.",
|
||||
link: "https://matrix.org",
|
||||
siteName: "GitHub",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
alt: "Element logo",
|
||||
playable: false,
|
||||
},
|
||||
};
|
||||
@ -13,7 +13,7 @@ import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./LinkPreview.stories.tsx";
|
||||
|
||||
const { Default, WithTooltip, Title, TitleAndDescription } = composeStories(stories);
|
||||
const { Default, WithTooltip, Title, TitleAndDescription, Video } = composeStories(stories);
|
||||
|
||||
describe("LinkPreview", () => {
|
||||
it("renders a preview", () => {
|
||||
@ -36,4 +36,10 @@ describe("LinkPreview", () => {
|
||||
// Tooltip has the URL
|
||||
expect(await screen.findByText("https://matrix.org/")).toBeVisible();
|
||||
});
|
||||
it("renders a playable preview that can be opened with a click", () => {
|
||||
const { container } = render(<Video />);
|
||||
expect(container).toMatchSnapshot();
|
||||
const button = screen.getByLabelText("Open link");
|
||||
expect(button).toHaveAttribute("href", "https://matrix.org");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type MouseEventHandler, type JSX, useCallback } from "react";
|
||||
import { Tooltip, Text, Avatar, Button } from "@vector-im/compound-web";
|
||||
import PlaySolidIcon from "@vector-im/compound-design-tokens/assets/web/icons/play-solid";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useI18n } from "../../../../../core/i18n/i18nContext";
|
||||
import type { UrlPreview } from "../types";
|
||||
import { LinkedText } from "../../../../../core/utils/LinkedText";
|
||||
import styles from "./LinkPreview.module.css";
|
||||
|
||||
export interface LinkPreviewActions {
|
||||
onImageClick: () => void;
|
||||
}
|
||||
|
||||
export type LinkPreviewProps = UrlPreview & LinkPreviewActions;
|
||||
|
||||
function LinkTitle({
|
||||
title,
|
||||
showTooltipOnLink,
|
||||
link,
|
||||
}: Pick<LinkPreviewProps, "title" | "showTooltipOnLink" | "link">): JSX.Element {
|
||||
const caption = new URL(link).toString();
|
||||
const anchor = (
|
||||
<Text
|
||||
as="a"
|
||||
type="body"
|
||||
weight="semibold"
|
||||
size="lg"
|
||||
className={styles.title}
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
return showTooltipOnLink ? <Tooltip label={caption}>{anchor}</Tooltip> : anchor;
|
||||
}
|
||||
|
||||
function LinkSiteName({ siteIcon, siteName }: { siteIcon?: string; siteName: string }): JSX.Element {
|
||||
return (
|
||||
<div className={styles.siteName}>
|
||||
{siteIcon && <Avatar size="16px" name={siteName} id={siteName} src={siteIcon} />}
|
||||
<Text as="span" size="sm" weight="regular">
|
||||
{siteName}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A condensed link preview that only contains the site icon, the title of the link and the site name.
|
||||
*/
|
||||
function LinkPreviewInline({
|
||||
title,
|
||||
showTooltipOnLink,
|
||||
siteIcon,
|
||||
siteName,
|
||||
link,
|
||||
}: Omit<LinkPreviewProps, "image" | "description" | "author" | "onImageClick">): JSX.Element {
|
||||
return (
|
||||
<div className={classNames(styles.container, styles.inline)}>
|
||||
{siteIcon && (
|
||||
<div className={styles.siteAvatar}>
|
||||
<Avatar type="square" size="48px" name={title} id={title} src={siteIcon} />
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames(styles.textContent, styles.inline)}>
|
||||
<LinkTitle title={title} showTooltipOnLink={showTooltipOnLink} link={link} />
|
||||
{siteName && <LinkSiteName siteName={siteName} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkPreview renders a single preview component for a single link on an event. It is usually rendered as part of
|
||||
* a `UrlPreviewGroupView`.
|
||||
*/
|
||||
export function LinkPreview({ onImageClick, ...preview }: LinkPreviewProps): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
const onImageClickHandler = useCallback<MouseEventHandler>(
|
||||
(ev) => {
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
|
||||
if (!preview.image?.imageFull) {
|
||||
return;
|
||||
}
|
||||
onImageClick();
|
||||
},
|
||||
[preview.image?.imageFull, onImageClick],
|
||||
);
|
||||
|
||||
if (!preview.image && !preview.author && !preview.description) {
|
||||
return <LinkPreviewInline {...preview} />;
|
||||
}
|
||||
|
||||
let img: JSX.Element | undefined;
|
||||
|
||||
if (preview.image) {
|
||||
if (preview.image.playable) {
|
||||
// Playable media do not have clickable images so we don't
|
||||
// overlay buttons atop buttons, instead we render a
|
||||
// button for them to open the media.
|
||||
img = (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url('${preview.image.imageThumb}')`,
|
||||
}}
|
||||
className={styles.preview}
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href={preview.link}
|
||||
aria-label={_t("timeline|url_preview|open_link")}
|
||||
className={styles.playButton}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
kind="primary"
|
||||
>
|
||||
<PlaySolidIcon width="24px" height="24px" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Otherwise, the preview can be clicked on.
|
||||
img = (
|
||||
<button
|
||||
className={styles.preview}
|
||||
onClick={onImageClickHandler}
|
||||
aria-label={_t("timeline|url_preview|view_image")}
|
||||
>
|
||||
<img src={preview.image.imageThumb} alt={preview.image.alt} title={preview.image.alt} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{img}
|
||||
<div className={styles.textContent}>
|
||||
{preview.author && (
|
||||
<Text as="span" size="md" weight="semibold">
|
||||
{preview.author}
|
||||
</Text>
|
||||
)}
|
||||
<LinkTitle title={preview.title} showTooltipOnLink={preview.showTooltipOnLink} link={preview.link} />
|
||||
<LinkedText type="body" size="md" className={styles.description}>
|
||||
{preview.description}
|
||||
</LinkedText>
|
||||
{preview.siteName && <LinkSiteName siteName={preview.siteName} siteIcon={preview.siteIcon} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`LinkPreview > renders a playable preview that can be opened with a click 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_preview"
|
||||
style="background-image: url("/static/wideImage.png");"
|
||||
>
|
||||
<a
|
||||
aria-label="Open link"
|
||||
class="_button_13vu4_8 LinkPreview-module_playButton"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="24px"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m8.98 4.677 9.921 5.58c1.36.764 1.36 2.722 0 3.486l-9.92 5.58C7.647 20.073 6 19.11 6 17.58V6.42c0-1.53 1.647-2.493 2.98-1.743"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A linked video
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
This is a link to a video. You cannot play the video inline yet, but you can click the play button to open the link
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
blog.example.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LinkPreview > renders a preview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="Element logo"
|
||||
src="/static/element.png"
|
||||
title="Element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
Site name
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LinkPreview > renders a preview with just a title 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container LinkPreview-module_inline"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_textContent LinkPreview-module_inline"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`LinkPreview > renders a preview with just a title and description 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description with a link to
|
||||
<a
|
||||
data-linkified="true"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://matrix.org
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -8,17 +8,17 @@
|
||||
import React, { type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import imageFile from "../../../static/element.png";
|
||||
import tallImageFile from "../../../static/tallImage.png";
|
||||
import imageFile from "../../../../../static/element.png";
|
||||
import tallImageFile from "../../../../../static/tallImage.png";
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import {
|
||||
UrlPreviewGroupView,
|
||||
type UrlPreviewGroupViewActions,
|
||||
type UrlPreviewGroupViewSnapshot,
|
||||
} from "./UrlPreviewGroupView";
|
||||
import { useMockedViewModel } from "../../core/viewmodel";
|
||||
import { LinkedTextContext } from "../../core/utils/LinkedText";
|
||||
import { withViewDocs } from "../../../.storybook/withViewDocs";
|
||||
import { useMockedViewModel } from "../../../../core/viewmodel";
|
||||
import { LinkedTextContext } from "../../../../core/utils/LinkedText";
|
||||
import { withViewDocs } from "../../../../../.storybook/withViewDocs";
|
||||
|
||||
type UrlPreviewGroupViewProps = UrlPreviewGroupViewSnapshot & UrlPreviewGroupViewActions;
|
||||
|
||||
@ -51,6 +51,12 @@ export default {
|
||||
onImageClick: fn(),
|
||||
onTogglePreviewLimit: fn(),
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/sI9A2kV2K4xeiyqJsL7Ey3/Link-Previews?node-id=87-7920",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof UrlPreviewGroupViewWrapper>;
|
||||
|
||||
const Template: StoryFn<typeof UrlPreviewGroupViewWrapper> = (args) => <UrlPreviewGroupViewWrapper {...args} />;
|
||||
@ -62,9 +68,13 @@ Default.args = {
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
link: "https://matrix.org",
|
||||
showTooltipOnLink: false,
|
||||
siteName: "matrix.org",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
alt: "The element logo",
|
||||
playable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -72,17 +82,7 @@ Default.args = {
|
||||
|
||||
export const MultiplePreviewsHidden = Template.bind({});
|
||||
MultiplePreviewsHidden.args = {
|
||||
previews: [
|
||||
{
|
||||
title: "A simple title",
|
||||
description: "A simple description",
|
||||
link: "https://matrix.org",
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
},
|
||||
},
|
||||
],
|
||||
previews: Default.args.previews,
|
||||
overPreviewLimit: true,
|
||||
previewsLimited: true,
|
||||
totalPreviewCount: 10,
|
||||
@ -95,9 +95,13 @@ MultiplePreviewsVisible.args = {
|
||||
title: "One",
|
||||
description: "A regular square image.",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
showTooltipOnLink: false,
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
alt: "The element logo",
|
||||
playable: false,
|
||||
},
|
||||
},
|
||||
// These images should appear the same size despite having different dimensions.
|
||||
@ -105,18 +109,26 @@ MultiplePreviewsVisible.args = {
|
||||
title: "Two",
|
||||
description: "This one has a taller image which should crop nicely.",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
showTooltipOnLink: false,
|
||||
image: {
|
||||
imageThumb: tallImageFile,
|
||||
imageFull: tallImageFile,
|
||||
alt: "A dog",
|
||||
playable: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Three",
|
||||
description: "One more description",
|
||||
link: "https://matrix.org",
|
||||
siteName: "matrix.org",
|
||||
showTooltipOnLink: false,
|
||||
image: {
|
||||
imageThumb: imageFile,
|
||||
imageFull: imageFile,
|
||||
alt: "The element logo",
|
||||
playable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -10,8 +10,8 @@ import { Button, IconButton } from "@vector-im/compound-web";
|
||||
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useViewModel, type ViewModel } from "../../core/viewmodel";
|
||||
import { useI18n } from "../../core/i18n/i18nContext";
|
||||
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
||||
import { useI18n } from "../../../../core/i18n/i18nContext";
|
||||
import type { UrlPreview } from "./types";
|
||||
import { LinkPreview } from "./LinkPreview";
|
||||
import styles from "./UrlPreviewGroupView.module.css";
|
||||
@ -68,7 +68,12 @@ export function UrlPreviewGroupView({ vm }: UrlPreviewGroupViewProps): JSX.Eleme
|
||||
))}
|
||||
{toggleButton}
|
||||
</div>
|
||||
<IconButton size="20px" onClick={vm.onHideClick} aria-label={_t("timeline|url_preview|close")}>
|
||||
<IconButton
|
||||
kind="secondary"
|
||||
size="28px"
|
||||
onClick={vm.onHideClick}
|
||||
aria-label={_t("timeline|url_preview|close")}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
@ -0,0 +1,500 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`UrlPreviewGroupView > renders a single preview 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders multiple previews 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
One
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A regular square image.
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="A dog"
|
||||
src="/static/tallImage.png"
|
||||
title="A dog"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Two
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
This one has a taller image which should crop nicely.
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Three
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
One more description
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Collapse
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders multiple previews which are hidden 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
A simple title
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A simple description
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Show 9 other previews
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UrlPreviewGroupView > renders with a compact view 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_wrapper"
|
||||
>
|
||||
<div
|
||||
class="UrlPreviewGroupView-module_previewGroup UrlPreviewGroupView-module_compactLayout"
|
||||
>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
One
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
A regular square image.
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="A dog"
|
||||
src="/static/tallImage.png"
|
||||
title="A dog"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Two
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
This one has a taller image which should crop nicely.
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="LinkPreview-module_container"
|
||||
>
|
||||
<button
|
||||
aria-label="View image"
|
||||
class="LinkPreview-module_preview"
|
||||
>
|
||||
<img
|
||||
alt="The element logo"
|
||||
src="/static/element.png"
|
||||
title="The element logo"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="LinkPreview-module_textContent"
|
||||
>
|
||||
<a
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 LinkPreview-module_title"
|
||||
href="https://matrix.org"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Three
|
||||
</a>
|
||||
<p
|
||||
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 LinkedText-module_container LinkPreview-module_description"
|
||||
>
|
||||
One more description
|
||||
</p>
|
||||
<div
|
||||
class="LinkPreview-module_siteName"
|
||||
>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-body-sm-regular_6v6n8_31"
|
||||
>
|
||||
matrix.org
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="_button_13vu4_8 UrlPreviewGroupView-module_toggleButton"
|
||||
data-kind="tertiary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Collapse
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Close preview"
|
||||
class="_icon-button_1215g_8"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -5,7 +5,6 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/** Represents a URL preview. */
|
||||
export interface UrlPreview {
|
||||
/**
|
||||
* The URL for the preview.
|
||||
@ -14,7 +13,7 @@ export interface UrlPreview {
|
||||
/**
|
||||
* Should the link have a tooltip. Should be `true` if the platform does not provide a tooltip.
|
||||
*/
|
||||
showTooltipOnLink?: boolean;
|
||||
showTooltipOnLink: boolean;
|
||||
/**
|
||||
* The title of the page being previewed.
|
||||
*/
|
||||
@ -22,7 +21,11 @@ export interface UrlPreview {
|
||||
/**
|
||||
* The site name to be displayed alongside the title.
|
||||
*/
|
||||
siteName?: string;
|
||||
siteName: string;
|
||||
/**
|
||||
* The HTTP URI of the the sites icon.
|
||||
*/
|
||||
siteIcon?: string;
|
||||
/**
|
||||
* Description of the site. May contain links.
|
||||
*/
|
||||
@ -44,12 +47,26 @@ export interface UrlPreview {
|
||||
*/
|
||||
fileSize?: number;
|
||||
/**
|
||||
* The width of the thumbnail. Must not exceed 100px.
|
||||
* The width of the thumbnail.
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* The height of the thumbnail. Must not exceed 100px.
|
||||
* The height of the thumbnail.
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Alt text for the image
|
||||
*/
|
||||
alt?: string;
|
||||
|
||||
/**
|
||||
* Is the media playable.
|
||||
*/
|
||||
playable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Author of the content, if specified.
|
||||
*/
|
||||
author?: string;
|
||||
}
|
||||
BIN
packages/shared-components/static/wideImage.png
Normal file
|
After Width: | Height: | Size: 40 KiB |