Files
cinny/src/app/components/RenderMessageContent.tsx

369 lines
11 KiB
TypeScript

import React from 'react';
import { MsgType } from 'matrix-js-sdk';
import { HTMLReactParserOptions } from 'html-react-parser';
import { Opts } from 'linkifyjs';
import { config } from 'folds';
import {
AudioContent,
DownloadFile,
FileContent,
ImageContent,
MAudio,
MBadEncrypted,
MEmote,
MFile,
MImage,
MLocation,
MNotice,
MText,
MVideo,
ReadPdfFile,
ReadTextFile,
RenderBody,
ThumbnailContent,
UnsupportedContent,
VideoContent,
VideoFrameThumbnail,
} from './message';
import { UrlPreviewCard, UrlPreviewHolder, YouTubeEmbed, isYouTubeUrl, TikTokEmbed, isTikTokUrl, GiphyEmbed, isGiphyUrl } from './url-preview';
import { Image, MediaControl, Video } from './media';
import { ImageViewer } from './image-viewer';
import { VideoViewer } from './video-viewer';
import { PdfViewer } from './Pdf-viewer';
import { TextViewer } from './text-viewer';
import { testMatrixTo } from '../plugins/matrix-to';
import { IImageContent } from '../../types/matrix/common';
import { filterDisabledUrls } from '../hooks/useRoomEmbedFilters';
import { pluginRegistry } from '../features/settings/plugins/PluginAPI';
type RenderMessageContentProps = {
displayName: string;
msgType: string;
ts: number;
edited?: boolean;
getContent: <T>() => T;
mediaAutoLoad?: boolean;
urlPreview?: boolean;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
linkifyOpts: Opts;
outlineAttachment?: boolean;
disabledEmbedPatterns?: string[];
};
export function RenderMessageContent({
displayName,
msgType,
ts,
edited,
getContent,
mediaAutoLoad,
urlPreview,
highlightRegex,
htmlReactParserOptions,
linkifyOpts,
outlineAttachment,
disabledEmbedPatterns,
}: RenderMessageContentProps) {
const renderUrlsPreview = (urls: string[]) => {
let filteredUrls = urls.filter((url) => !testMatrixTo(url));
filteredUrls = filterDisabledUrls(filteredUrls, disabledEmbedPatterns ?? []);
if (filteredUrls.length === 0) return undefined;
// Separate special URLs from generic ones
const youtubeUrls = filteredUrls.filter(isYouTubeUrl);
const tiktokUrls = filteredUrls.filter(isTikTokUrl);
const giphyUrls = filteredUrls.filter(isGiphyUrl);
const otherUrls = filteredUrls.filter((url) => !isYouTubeUrl(url) && !isTikTokUrl(url) && !isGiphyUrl(url));
return (
<>
{/* Render YouTube embeds (ad-free via yt-dlp) */}
{youtubeUrls.map((url) => (
<YouTubeEmbed key={url} url={url} />
))}
{/* Render TikTok embeds */}
{tiktokUrls.map((url) => (
<TikTokEmbed key={url} url={url} />
))}
{/* Render Giphy embeds */}
{giphyUrls.map((url) => (
<GiphyEmbed key={url} url={url} />
))}
{/* Render standard URL previews for other links */}
{otherUrls.length > 0 && (
<UrlPreviewHolder>
{otherUrls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
)}
</>
);
};
const renderCaption = () => {
const content: IImageContent = getContent();
if (content.filename && content.filename !== content.body) {
return (
<MText
style={{ marginTop: config.space.S200 }}
edited={edited}
content={content}
renderBody={(props) => (
<RenderBody
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
return null;
};
const renderFile = () => (
<>
<MFile
content={getContent()}
renderFileContent={({ body, mimeType, info, encInfo, url }) => (
<FileContent
body={body}
mimeType={mimeType}
renderAsPdfFile={() => (
<ReadPdfFile
body={body}
mimeType={mimeType}
url={url}
encInfo={encInfo}
renderViewer={(p) => <PdfViewer {...p} />}
/>
)}
renderAsTextFile={() => (
<ReadTextFile
body={body}
mimeType={mimeType}
url={url}
encInfo={encInfo}
renderViewer={(p) => <TextViewer {...p} />}
/>
)}
>
<DownloadFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} info={info} />
</FileContent>
)}
outlined={outlineAttachment}
/>
{renderCaption()}
</>
);
// Check for custom renderer from plugins BEFORE standard type handling
// so plugins can intercept any msgtype including custom ones.
const customRendererEarly = pluginRegistry.getRenderer('message');
if (customRendererEarly) {
try {
const messageDataEarly = {
msgtype: msgType,
...getContent(),
};
const customContentEarly = customRendererEarly(messageDataEarly, () => null);
if (customContentEarly) {
return customContentEarly as React.ReactElement;
}
} catch (error) {
console.error('[RenderMessageContent] Custom renderer error (early):', error);
}
}
if (msgType === MsgType.Text) {
return (
<MText
edited={edited}
content={getContent()}
renderBody={(props) => (
<RenderBody
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
if (msgType === MsgType.Emote) {
return (
<MEmote
displayName={displayName}
edited={edited}
content={getContent()}
renderBody={(props) => (
<RenderBody
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
if (msgType === MsgType.Notice) {
return (
<MNotice
edited={edited}
content={getContent()}
renderBody={(props) => (
<RenderBody
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
}
if (msgType === MsgType.Image) {
return (
<>
<MImage
content={getContent()}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
outlined={outlineAttachment}
/>
{renderCaption()}
</>
);
}
if (msgType === MsgType.Video) {
return (
<>
<MVideo
content={getContent()}
renderAsFile={renderFile}
renderVideoContent={({ body, info, mimeType, url, encInfo, ...props }) => {
const hasValidThumbnail =
(typeof info.thumbnail_file?.url === 'string' || typeof info.thumbnail_url === 'string') &&
typeof info.thumbnail_info?.mimetype === 'string';
return (
<VideoContent
body={body}
info={info}
mimeType={mimeType}
url={url}
encInfo={encInfo}
autoPlay={mediaAutoLoad}
{...props}
renderThumbnail={
mediaAutoLoad
? () => hasValidThumbnail ? (
<ThumbnailContent
info={info}
renderImage={(src) => (
<Image alt={body} title={body} src={src} loading="lazy" />
)}
/>
) : (
<VideoFrameThumbnail
mimeType={mimeType}
url={url}
encInfo={encInfo}
renderImage={(src) => (
<Image alt={body} title={body} src={src} loading="lazy" />
)}
/>
)
: undefined
}
renderViewer={(p) => <VideoViewer {...p} />}
renderVideo={(p) => <Video {...p} />}
/>
);
}}
outlined={outlineAttachment}
/>
{renderCaption()}
</>
);
}
if (msgType === MsgType.Audio) {
return (
<>
<MAudio
content={getContent()}
renderAsFile={renderFile}
renderAudioContent={(props) => (
<AudioContent {...props} renderMediaControl={(p) => <MediaControl {...p} />} />
)}
outlined={outlineAttachment}
/>
{renderCaption()}
</>
);
}
if (msgType === MsgType.File) {
return renderFile();
}
if (msgType === MsgType.Location) {
return <MLocation content={getContent()} />;
}
if (msgType === 'm.bad.encrypted') {
return <MBadEncrypted />;
}
// Check for custom renderer from plugins FIRST
const customRenderer = pluginRegistry.getRenderer('message');
console.log('[RenderMessageContent] customRenderer function:', typeof customRenderer);
if (customRenderer) {
try {
const messageData = {
msgtype: msgType,
...getContent(),
};
console.log('[RenderMessageContent] Calling customRenderer with msgtype:', msgType);
console.log('[RenderMessageContent] messageData:', messageData);
const customContent = customRenderer(
messageData,
() => {
console.log('[RenderMessageContent] defaultRenderer called');
return null;
}
);
console.log('[RenderMessageContent] customRenderer returned:', customContent);
console.log('[RenderMessageContent] customContent type:', typeof customContent);
if (customContent) {
console.log('[RenderMessageContent] Returning custom content');
return customContent as React.ReactElement;
}
} catch (error) {
console.error('[RenderMessageContent] Custom renderer error:', error);
}
} else {
console.log('[RenderMessageContent] No custom renderer registered');
}
return <UnsupportedContent />;
}