Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

134 lines
4.1 KiB
TypeScript

import type { IMessage } from '@rocket.chat/core-typings';
import { MessageBlock } from '@rocket.chat/fuselage';
import { useOembedLayout } from '../hooks/useOembedLayout';
import type { OEmbedPreviewMetadata } from './urlPreviews/OEmbedPreviewMetadata';
import OEmbedResolver from './urlPreviews/OEmbedResolver';
import UrlPreview from './urlPreviews/UrlPreview';
import type { UrlPreviewMetadata } from './urlPreviews/UrlPreviewMetadata';
import { buildImageURL } from './urlPreviews/buildImageURL';
type OembedUrlLegacy = Required<IMessage>['urls'][0];
type PreviewTypes = 'headers' | 'oembed';
type PreviewData = {
type: PreviewTypes;
data: OEmbedPreviewMetadata | UrlPreviewMetadata;
};
const normalizeMeta = ({ url, meta }: { url: string; meta: Record<string, string> }): OEmbedPreviewMetadata => {
const image = meta.ogImage || meta.twitterImage || meta.msapplicationTileImage || meta.oembedThumbnailUrl || meta.oembedThumbnailUrl;
const imageHeight = meta.ogImageHeight || meta.oembedHeight || meta.oembedThumbnailHeight;
const imageWidth = meta.ogImageWidth || meta.oembedWidth || meta.oembedThumbnailWidth;
return Object.fromEntries(
Object.entries({
siteName: meta.ogSiteName || meta.oembedProviderName,
siteUrl: meta.ogUrl || meta.oembedProviderUrl,
title: meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle,
description: meta.ogDescription || meta.twitterDescription || meta.description,
authorName: meta.oembedAuthorName,
authorUrl: meta.oembedAuthorUrl,
...(image && {
image: {
url: buildImageURL(url, image),
dimensions: {
...(imageHeight && { height: imageHeight }),
...(imageWidth && { width: imageWidth }),
},
},
}),
url: meta.oembedUrl || url,
type: meta.ogType || meta.oembedType,
...(meta.oembedHtml && { html: meta.oembedHtml }),
}).filter(([, value]) => value),
);
};
const hasContentType = (headers: OembedUrlLegacy['headers']): headers is { contentType: string } =>
headers ? 'contentType' in headers : false;
const getHeaderType = (headers: OembedUrlLegacy['headers']): UrlPreviewMetadata['type'] | undefined => {
if (!hasContentType(headers)) {
return;
}
if (headers.contentType.match(/image\/.*/)) {
return 'image';
}
if (headers.contentType.match(/audio\/.*/)) {
return 'audio';
}
if (headers.contentType.match(/video\/.*/)) {
return 'video';
}
};
const isValidPreviewMeta = ({
siteName,
siteUrl,
authorName,
authorUrl,
title,
description,
image,
html,
}: OEmbedPreviewMetadata): boolean =>
!((!siteName || !siteUrl) && (!authorName || !authorUrl) && !title && !description && !image && !html);
const hasMeta = (url: OembedUrlLegacy): url is { url: string; meta: Record<string, string> } => !!url.meta && !!Object.values(url.meta);
const processMetaAndHeaders = (url: OembedUrlLegacy): PreviewData | false => {
if (!url.headers && !url.meta) {
return false;
}
const data = hasMeta(url) ? normalizeMeta(url) : undefined;
if (data && isValidPreviewMeta(data)) {
return { type: 'oembed', data };
}
const type = getHeaderType(url.headers);
if (!type) {
return false;
}
return {
type: 'headers',
data: { url: url.url, type, originalType: hasContentType(url.headers) ? url.headers?.contentType : '' },
};
};
const isPreviewData = (data: PreviewData | false): data is PreviewData => !!data;
const isMetaPreview = (_data: PreviewData['data'], type: PreviewTypes): _data is OEmbedPreviewMetadata => type === 'oembed';
export type UrlPreviewsProps = { urls: OembedUrlLegacy[] };
const UrlPreviews = ({ urls }: UrlPreviewsProps) => {
const { maxWidth: oembedMaxWidth } = useOembedLayout();
const metaAndHeaders = urls.map(processMetaAndHeaders).filter(isPreviewData);
return (
<>
{metaAndHeaders.map(({ type, data }, index) => {
if (isMetaPreview(data, type)) {
return (
<MessageBlock width='100%' maxWidth={oembedMaxWidth} key={index}>
<OEmbedResolver meta={data} />
</MessageBlock>
);
}
return (
<MessageBlock width='100%' maxWidth={oembedMaxWidth} key={index}>
<UrlPreview {...data} />
</MessageBlock>
);
})}
</>
);
};
export default UrlPreviews;