369 lines
11 KiB
TypeScript
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 />;
|
|
}
|