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
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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}>;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type UrlPreviewMetadata = {
|
||||
type: 'image' | 'video' | 'audio';
|
||||
originalType: string;
|
||||
url: string;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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't support the video element.
|
||||
<track kind='captions' />
|
||||
</video>
|
||||
);
|
||||
|
||||
export default UrlVideoPreview;
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user