snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { MessageGenericPreview } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import OEmbedPreviewContent from './OEmbedPreviewContent';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
import MessageCollapsible from '../../MessageCollapsible';
export type OEmbedCollapsibleProps = { children?: ReactNode } & OEmbedPreviewMetadata;
const OEmbedCollapsible = ({ children, ...props }: OEmbedCollapsibleProps) => {
const { t } = useTranslation();
return (
<MessageCollapsible title={t('Link_Preview')}>
<MessageGenericPreview>
{children}
<OEmbedPreviewContent {...props} />
</MessageGenericPreview>
</MessageCollapsible>
);
};
export default OEmbedCollapsible;

View File

@@ -0,0 +1,24 @@
import { Box } from '@rocket.chat/fuselage';
import DOMPurify from 'dompurify';
import { useMemo } from 'react';
import OEmbedCollapsible from './OEmbedCollapsible';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
const purifyOptions = {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['frameborder', 'allow', 'allowfullscreen', 'scrolling', 'src', 'style', 'referrerpolicy'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata) => {
// Memoize the dangerouslySetInnerHTML object so its identity is stable across
// re-renders. React 19 re-applies innerHTML when this object is a new reference
// (even with an identical string), which reloads embedded iframes (e.g. YouTube)
// on every re-render — visible as a flicker on new messages / reactions.
const dangerous = useMemo(() => (html ? { __html: DOMPurify.sanitize(html, purifyOptions) } : undefined), [html]);
return <OEmbedCollapsible {...props}>{dangerous && <Box withRichContent dangerouslySetInnerHTML={dangerous} />}</OEmbedCollapsible>;
};
export default OEmbedHtmlPreview;

View File

@@ -0,0 +1,12 @@
import { MessageGenericPreviewCoverImage } from '@rocket.chat/fuselage';
import OEmbedCollapsible from './OEmbedCollapsible';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
const OEmbedImagePreview = ({ image, ...props }: OEmbedPreviewMetadata) => (
<OEmbedCollapsible {...props}>
{image?.url && <MessageGenericPreviewCoverImage height={192} width={368} url={image?.url} />}
</OEmbedCollapsible>
);
export default OEmbedImagePreview;

View File

@@ -0,0 +1,17 @@
import { MessageGenericPreviewCoverImage } from '@rocket.chat/fuselage';
import { ExternalLink } from '@rocket.chat/ui-client';
import OEmbedCollapsible from './OEmbedCollapsible';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
const OEmbedLinkPreview = ({ image, url, ...props }: OEmbedPreviewMetadata) => (
<OEmbedCollapsible url={url} {...props}>
{image?.url && url && (
<ExternalLink to={url}>
<MessageGenericPreviewCoverImage height={192} width={368} url={image?.url} />
</ExternalLink>
)}
</OEmbedCollapsible>
);
export default OEmbedLinkPreview;

View File

@@ -0,0 +1,41 @@
import {
MessageGenericPreviewContent,
MessageGenericPreviewTitle,
MessageGenericPreviewDescription,
MessageGenericPreviewFooter,
Box,
} from '@rocket.chat/fuselage';
import type { ReactElement, ReactNode } from 'react';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
import MarkdownText from '../../../MarkdownText';
export type OEmbedPreviewContentProps = { thumb?: ReactElement<any>; children?: ReactNode } & OEmbedPreviewMetadata;
const OEmbedPreviewContent = ({ title, description, url, thumb, authorName, authorUrl, siteName, siteUrl }: OEmbedPreviewContentProps) => {
const showSiteName = siteName && siteUrl;
const showAuthorName = authorName && authorUrl;
const showFooterSeparator = showSiteName && showAuthorName;
return (
<MessageGenericPreviewContent thumb={thumb}>
{title && (
<MessageGenericPreviewTitle externalUrl={url} title={title}>
{title}
</MessageGenericPreviewTitle>
)}
{description && <MessageGenericPreviewDescription>{description}</MessageGenericPreviewDescription>}
{(showSiteName || showAuthorName) && (
<MessageGenericPreviewFooter>
<Box display='flex' justifyContent='flex-start'>
{showSiteName && <MarkdownText variant='inline' content={`[${siteName}](${siteUrl})`} />}
{showFooterSeparator && <Box marginInline={4}>|</Box>}
{showAuthorName && <MarkdownText variant='inline' content={`[${authorName}](${authorUrl})`} />}
</Box>
</MessageGenericPreviewFooter>
)}
</MessageGenericPreviewContent>
);
};
export default OEmbedPreviewContent;

View File

@@ -0,0 +1,22 @@
type PreviewType = 'photo' | 'video' | 'link' | 'rich';
export type OEmbedPreviewMetadata = Partial<{
siteName: string; // nome
siteUrl: string; // parsedUrl.host
siteColor: string; // ogColor
title: string; // ogTitle
description: string; // ogDescription
authorName: string;
authorUrl?: string;
image: {
// preview?: string; // base64 low res preview
url: string;
dimensions: {
width?: number;
height?: number;
};
};
url: string;
type: PreviewType;
html?: string; // for embedded OembedType
}>;

View File

@@ -0,0 +1,24 @@
import OEmbedHtmlPreview from './OEmbedHtmlPreview';
import OEmbedImagePreview from './OEmbedImagePreview';
import OEmbedLinkPreview from './OEmbedLinkPreview';
import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata';
export type OEmbedResolverProps = {
meta: OEmbedPreviewMetadata;
};
const OEmbedResolver = ({ meta }: OEmbedResolverProps) => {
switch (meta.type) {
case 'rich':
case 'video':
return <OEmbedHtmlPreview {...meta} />;
case 'photo':
return <OEmbedImagePreview {...meta} />;
default:
return <OEmbedLinkPreview {...meta} />;
}
};
export default OEmbedResolver;

View File

@@ -0,0 +1,9 @@
import { AudioPlayer } from '@rocket.chat/fuselage';
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';
export type UrlAudioPreviewProps = Pick<UrlPreviewMetadata, 'url'>;
const UrlAudioPreview = ({ url }: UrlAudioPreviewProps) => <AudioPlayer src={url} />;
export default UrlAudioPreview;

View File

@@ -0,0 +1,16 @@
import { Box, MessageGenericPreviewImage } from '@rocket.chat/fuselage';
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';
import { useOembedLayout } from '../../hooks/useOembedLayout';
const UrlImagePreview = ({ url }: Pick<UrlPreviewMetadata, 'url'>) => {
const { maxHeight: oembedMaxHeight } = useOembedLayout();
return (
<Box maxHeight={oembedMaxHeight} maxWidth='100%'>
<MessageGenericPreviewImage data-id={url} className='preview-image' url={url || ''} alt='' />
</Box>
);
};
export default UrlImagePreview;

View File

@@ -0,0 +1,25 @@
import { Box } from '@rocket.chat/fuselage';
import { useAttachmentAutoLoadEmbedMedia } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';
import UrlPreviewResolver from './UrlPreviewResolver';
import { useCollapse } from '../../hooks/useCollapse';
import CollapsibleContent from '../collapsible/CollapsibleContent';
const UrlPreview = (props: UrlPreviewMetadata) => {
const autoLoadMedia = useAttachmentAutoLoadEmbedMedia();
const [collapsed, toggleCollapse] = useCollapse(!autoLoadMedia);
const { t } = useTranslation();
return (
<>
<Box display='flex' flexDirection='row' color='hint' fontScale='c1' alignItems='center'>
{t('Link_Preview')} <CollapsibleContent key='collapsible-content-action' collapsed={collapsed} onClick={toggleCollapse} />
</Box>
{!collapsed && <UrlPreviewResolver {...props} />}
</>
);
};
export default UrlPreview;

View File

@@ -0,0 +1,5 @@
export type UrlPreviewMetadata = {
type: 'image' | 'video' | 'audio';
originalType: string;
url: string;
};

View File

@@ -0,0 +1,19 @@
import UrlAudioPreview from './UrlAudioPreview';
import UrlImagePreview from './UrlImagePreview';
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';
import UrlVideoPreview from './UrlVideoPreview';
const UrlPreviewResolver = ({ url, type, originalType }: UrlPreviewMetadata) => {
switch (type) {
case 'audio':
return <UrlAudioPreview url={url} />;
case 'video':
return <UrlVideoPreview url={url} originalType={originalType} />;
case 'image':
return <UrlImagePreview url={url} />;
default:
return null;
}
};
export default UrlPreviewResolver;

View File

@@ -0,0 +1,12 @@
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';
const style = { maxWidth: '100%' };
const UrlVideoPreview = ({ url, originalType }: Omit<UrlPreviewMetadata, 'type'>) => (
<video controls style={style}>
<source src={url} type={originalType} />
Your browser doesn&apos;t support the video element.
<track kind='captions' />
</video>
);
export default UrlVideoPreview;

View File

@@ -0,0 +1,20 @@
import { buildImageURL } from './buildImageURL';
const testCases = [
[
'https://g1.globo.com/mundo/video/misseis-atingem-ponte-de-vidro-em-kiev-11012523.ghtml',
'https://s2.glbimg.com/fXQKM_UZjF6I_3APIbPJzJTOUvw=/1200x/smart/filters:cover():strip_icc()/s04.video.glbimg.com/x720/11012523.jpg',
'https://s2.glbimg.com/fXQKM_UZjF6I_3APIbPJzJTOUvw=/1200x/smart/filters:cover():strip_icc()/s04.video.glbimg.com/x720/11012523.jpg',
],
['https://open.rocket.chat/channel/general', 'assets/favicon_512.png', 'https://open.rocket.chat/assets/favicon_512.png'],
['https://open.rocket.chat/channel/general', '/assets/favicon_512.png', 'https://open.rocket.chat/assets/favicon_512.png'],
['https://open.rocket.chat/channel/general/', '/assets/favicon_512.png', 'https://open.rocket.chat/assets/favicon_512.png'],
] as const;
testCases.forEach(([linkUrl, metaImgUrl, expectedResult]) => {
it(`should return ${expectedResult} for ${metaImgUrl}`, () => {
const result = buildImageURL(linkUrl, metaImgUrl);
expect(result).toBe(JSON.stringify(expectedResult));
});
});

View File

@@ -0,0 +1,13 @@
import { isValidLink } from '../../../../views/room/MessageList/lib/isValidLink';
export const buildImageURL = (url: string, imageUrl: string): string => {
if (isValidLink(imageUrl)) {
return JSON.stringify(imageUrl);
}
const { origin } = new URL(url);
const imgURL = `${origin}/${imageUrl}`;
const normalizedUrl = imgURL.replace(/([^:]\/)\/+/gm, '$1');
return JSON.stringify(normalizedUrl);
};