2024-05-31 19:49:46 +05:30
|
|
|
import React from 'react';
|
|
|
|
|
import { MsgType } from 'matrix-js-sdk';
|
|
|
|
|
import { HTMLReactParserOptions } from 'html-react-parser';
|
2024-07-30 17:48:59 +05:30
|
|
|
import { Opts } from 'linkifyjs';
|
2025-03-22 19:21:49 +11:00
|
|
|
import { config } from 'folds';
|
2024-05-31 19:49:46 +05:30
|
|
|
import {
|
|
|
|
|
AudioContent,
|
|
|
|
|
DownloadFile,
|
|
|
|
|
FileContent,
|
|
|
|
|
ImageContent,
|
|
|
|
|
MAudio,
|
|
|
|
|
MBadEncrypted,
|
|
|
|
|
MEmote,
|
|
|
|
|
MFile,
|
|
|
|
|
MImage,
|
|
|
|
|
MLocation,
|
|
|
|
|
MNotice,
|
|
|
|
|
MText,
|
|
|
|
|
MVideo,
|
|
|
|
|
ReadPdfFile,
|
|
|
|
|
ReadTextFile,
|
|
|
|
|
RenderBody,
|
|
|
|
|
ThumbnailContent,
|
|
|
|
|
UnsupportedContent,
|
2026-05-23 11:15:49 -04:00
|
|
|
VerificationRequestContent,
|
2024-05-31 19:49:46 +05:30
|
|
|
VideoContent,
|
|
|
|
|
} from './message';
|
|
|
|
|
import { UrlPreviewCard, UrlPreviewHolder } from './url-preview';
|
|
|
|
|
import { Image, MediaControl, Video } from './media';
|
|
|
|
|
import { ImageViewer } from './image-viewer';
|
|
|
|
|
import { PdfViewer } from './Pdf-viewer';
|
|
|
|
|
import { TextViewer } from './text-viewer';
|
2024-07-30 17:48:59 +05:30
|
|
|
import { testMatrixTo } from '../plugins/matrix-to';
|
2026-07-03 22:44:09 -04:00
|
|
|
import { IAudioContent, IFileContent, IImageContent } from '../../types/matrix/common';
|
|
|
|
|
|
|
|
|
|
// Audio is frequently sent as m.file (bridges/other clients, or when the browser
|
|
|
|
|
// reported a non-audio/* mime on upload). Detect that so we can play it inline
|
|
|
|
|
// like m.audio instead of showing only a download button.
|
|
|
|
|
const AUDIO_EXT_MIME: Record<string, string> = {
|
|
|
|
|
mp3: 'audio/mpeg',
|
|
|
|
|
m4a: 'audio/mp4',
|
|
|
|
|
aac: 'audio/aac',
|
|
|
|
|
oga: 'audio/ogg',
|
|
|
|
|
ogg: 'audio/ogg',
|
|
|
|
|
opus: 'audio/ogg',
|
|
|
|
|
wav: 'audio/wav',
|
|
|
|
|
flac: 'audio/flac',
|
|
|
|
|
weba: 'audio/webm',
|
|
|
|
|
};
|
|
|
|
|
const resolveInlineAudioMime = (content: IFileContent): string | undefined => {
|
|
|
|
|
const mime = content.info?.mimetype;
|
|
|
|
|
if (typeof mime === 'string' && mime.startsWith('audio')) return mime;
|
|
|
|
|
const name = content.filename ?? content.body ?? '';
|
|
|
|
|
const ext = name.split('.').pop()?.toLowerCase();
|
|
|
|
|
return ext ? AUDIO_EXT_MIME[ext] : undefined;
|
|
|
|
|
};
|
2024-05-31 19:49:46 +05:30
|
|
|
|
|
|
|
|
type RenderMessageContentProps = {
|
|
|
|
|
displayName: string;
|
|
|
|
|
msgType: string;
|
|
|
|
|
ts: number;
|
|
|
|
|
edited?: boolean;
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick?: () => void;
|
2024-05-31 19:49:46 +05:30
|
|
|
getContent: <T>() => T;
|
|
|
|
|
mediaAutoLoad?: boolean;
|
|
|
|
|
urlPreview?: boolean;
|
|
|
|
|
highlightRegex?: RegExp;
|
|
|
|
|
htmlReactParserOptions: HTMLReactParserOptions;
|
2024-07-30 17:48:59 +05:30
|
|
|
linkifyOpts: Opts;
|
2024-05-31 19:49:46 +05:30
|
|
|
outlineAttachment?: boolean;
|
2026-06-04 15:51:18 -04:00
|
|
|
eventId?: string;
|
2024-05-31 19:49:46 +05:30
|
|
|
};
|
|
|
|
|
export function RenderMessageContent({
|
|
|
|
|
displayName,
|
|
|
|
|
msgType,
|
|
|
|
|
ts,
|
|
|
|
|
edited,
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick,
|
2024-05-31 19:49:46 +05:30
|
|
|
getContent,
|
|
|
|
|
mediaAutoLoad,
|
|
|
|
|
urlPreview,
|
|
|
|
|
highlightRegex,
|
|
|
|
|
htmlReactParserOptions,
|
2024-07-30 17:48:59 +05:30
|
|
|
linkifyOpts,
|
2024-05-31 19:49:46 +05:30
|
|
|
outlineAttachment,
|
2026-06-04 15:51:18 -04:00
|
|
|
eventId,
|
2024-05-31 19:49:46 +05:30
|
|
|
}: RenderMessageContentProps) {
|
2024-07-30 17:48:59 +05:30
|
|
|
const renderUrlsPreview = (urls: string[]) => {
|
2026-07-07 00:45:51 -04:00
|
|
|
// Cap previews per message so a link-dump doesn't spawn dozens of preview
|
|
|
|
|
// fetches + iframes at once.
|
|
|
|
|
const filteredUrls = urls.filter((url) => !testMatrixTo(url)).slice(0, 6);
|
2024-07-30 17:48:59 +05:30
|
|
|
if (filteredUrls.length === 0) return undefined;
|
|
|
|
|
return (
|
|
|
|
|
<UrlPreviewHolder>
|
|
|
|
|
{filteredUrls.map((url) => (
|
|
|
|
|
<UrlPreviewCard key={url} url={url} ts={ts} />
|
|
|
|
|
))}
|
|
|
|
|
</UrlPreviewHolder>
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-01-06 01:44:22 +00:00
|
|
|
const renderCaption = () => {
|
|
|
|
|
const content: IImageContent = getContent();
|
2025-03-22 19:21:49 +11:00
|
|
|
if (content.filename && content.filename !== content.body) {
|
2025-01-06 01:44:22 +00:00
|
|
|
return (
|
|
|
|
|
<MText
|
2025-03-22 19:21:49 +11:00
|
|
|
style={{ marginTop: config.space.S200 }}
|
2025-01-06 01:44:22 +00:00
|
|
|
edited={edited}
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick={onEditHistoryClick}
|
2025-01-06 01:44:22 +00:00
|
|
|
content={content}
|
|
|
|
|
renderBody={(props) => (
|
|
|
|
|
<RenderBody
|
|
|
|
|
{...props}
|
|
|
|
|
highlightRegex={highlightRegex}
|
|
|
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
|
|
|
|
linkifyOpts={linkifyOpts}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2025-01-06 01:44:22 +00:00
|
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
|
|
|
|
/>
|
2025-03-22 19:21:49 +11:00
|
|
|
);
|
2025-01-06 01:44:22 +00:00
|
|
|
}
|
|
|
|
|
return null;
|
2025-03-22 19:21:49 +11:00
|
|
|
};
|
2025-01-06 01:44:22 +00:00
|
|
|
|
|
|
|
|
const renderFile = () => (
|
|
|
|
|
<>
|
|
|
|
|
<MFile
|
|
|
|
|
content={getContent()}
|
|
|
|
|
renderFileContent={({ body, mimeType, info, encInfo, url }) => (
|
2025-03-22 19:21:49 +11:00
|
|
|
<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>
|
2025-01-06 01:44:22 +00:00
|
|
|
)}
|
|
|
|
|
outlined={outlineAttachment}
|
|
|
|
|
/>
|
|
|
|
|
{renderCaption()}
|
|
|
|
|
</>
|
2024-05-31 19:49:46 +05:30
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Text) {
|
|
|
|
|
return (
|
|
|
|
|
<MText
|
|
|
|
|
edited={edited}
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick={onEditHistoryClick}
|
2024-05-31 19:49:46 +05:30
|
|
|
content={getContent()}
|
|
|
|
|
renderBody={(props) => (
|
|
|
|
|
<RenderBody
|
|
|
|
|
{...props}
|
|
|
|
|
highlightRegex={highlightRegex}
|
|
|
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
2024-07-30 17:48:59 +05:30
|
|
|
linkifyOpts={linkifyOpts}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2024-07-30 17:48:59 +05:30
|
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
2026-06-04 15:51:18 -04:00
|
|
|
eventId={eventId}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Emote) {
|
|
|
|
|
return (
|
|
|
|
|
<MEmote
|
|
|
|
|
displayName={displayName}
|
|
|
|
|
edited={edited}
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick={onEditHistoryClick}
|
2024-05-31 19:49:46 +05:30
|
|
|
content={getContent()}
|
|
|
|
|
renderBody={(props) => (
|
|
|
|
|
<RenderBody
|
|
|
|
|
{...props}
|
|
|
|
|
highlightRegex={highlightRegex}
|
|
|
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
2024-07-30 17:48:59 +05:30
|
|
|
linkifyOpts={linkifyOpts}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2024-07-30 17:48:59 +05:30
|
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
2026-06-04 15:51:18 -04:00
|
|
|
eventId={eventId}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Notice) {
|
|
|
|
|
return (
|
|
|
|
|
<MNotice
|
|
|
|
|
edited={edited}
|
2026-06-01 17:21:11 -04:00
|
|
|
onEditHistoryClick={onEditHistoryClick}
|
2024-05-31 19:49:46 +05:30
|
|
|
content={getContent()}
|
|
|
|
|
renderBody={(props) => (
|
|
|
|
|
<RenderBody
|
|
|
|
|
{...props}
|
|
|
|
|
highlightRegex={highlightRegex}
|
|
|
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
2024-07-30 17:48:59 +05:30
|
|
|
linkifyOpts={linkifyOpts}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
)}
|
2024-07-30 17:48:59 +05:30
|
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
2026-06-04 15:51:18 -04:00
|
|
|
eventId={eventId}
|
2024-05-31 19:49:46 +05:30
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Image) {
|
|
|
|
|
return (
|
2025-01-06 01:44:22 +00:00
|
|
|
<>
|
|
|
|
|
<MImage
|
|
|
|
|
content={getContent()}
|
|
|
|
|
renderImageContent={(props) => (
|
2025-03-22 19:21:49 +11:00
|
|
|
<ImageContent
|
|
|
|
|
{...props}
|
|
|
|
|
autoPlay={mediaAutoLoad}
|
|
|
|
|
renderImage={(p) => <Image {...p} loading="lazy" />}
|
|
|
|
|
renderViewer={(p) => <ImageViewer {...p} />}
|
|
|
|
|
/>
|
2025-01-06 01:44:22 +00:00
|
|
|
)}
|
|
|
|
|
outlined={outlineAttachment}
|
|
|
|
|
/>
|
|
|
|
|
{renderCaption()}
|
|
|
|
|
</>
|
2024-05-31 19:49:46 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Video) {
|
|
|
|
|
return (
|
2025-01-06 01:44:22 +00:00
|
|
|
<>
|
|
|
|
|
<MVideo
|
|
|
|
|
content={getContent()}
|
|
|
|
|
renderAsFile={renderFile}
|
2025-09-24 23:41:35 -04:00
|
|
|
renderVideoContent={({ body, info, ...props }) => (
|
2025-01-06 01:44:22 +00:00
|
|
|
<VideoContent
|
|
|
|
|
body={body}
|
|
|
|
|
info={info}
|
2025-09-24 23:41:35 -04:00
|
|
|
{...props}
|
2025-01-06 01:44:22 +00:00
|
|
|
renderThumbnail={
|
|
|
|
|
mediaAutoLoad
|
|
|
|
|
? () => (
|
2025-03-22 19:21:49 +11:00
|
|
|
<ThumbnailContent
|
|
|
|
|
info={info}
|
|
|
|
|
renderImage={(src) => (
|
2026-06-18 15:32:17 -04:00
|
|
|
<Image
|
|
|
|
|
alt={body}
|
|
|
|
|
title={body}
|
|
|
|
|
src={src}
|
|
|
|
|
loading="lazy"
|
2026-06-19 16:41:57 -04:00
|
|
|
style={{
|
|
|
|
|
objectFit: 'cover',
|
|
|
|
|
objectPosition: 'center top',
|
|
|
|
|
width: '100%',
|
|
|
|
|
height: '100%',
|
|
|
|
|
}}
|
2026-06-18 15:32:17 -04:00
|
|
|
/>
|
2025-03-22 19:21:49 +11:00
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
)
|
2025-01-06 01:44:22 +00:00
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
renderVideo={(p) => <Video {...p} />}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
outlined={outlineAttachment}
|
|
|
|
|
/>
|
|
|
|
|
{renderCaption()}
|
|
|
|
|
</>
|
2024-05-31 19:49:46 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Audio) {
|
|
|
|
|
return (
|
2025-01-06 01:44:22 +00:00
|
|
|
<>
|
|
|
|
|
<MAudio
|
|
|
|
|
content={getContent()}
|
|
|
|
|
renderAsFile={renderFile}
|
|
|
|
|
renderAudioContent={(props) => (
|
|
|
|
|
<AudioContent {...props} renderMediaControl={(p) => <MediaControl {...p} />} />
|
|
|
|
|
)}
|
|
|
|
|
outlined={outlineAttachment}
|
|
|
|
|
/>
|
|
|
|
|
{renderCaption()}
|
|
|
|
|
</>
|
2024-05-31 19:49:46 +05:30
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.File) {
|
2026-07-03 22:44:09 -04:00
|
|
|
// If an m.file is actually audio, play it inline (like m.audio) instead of
|
|
|
|
|
// only offering a download. MAudio falls back to renderFile if playback fails.
|
|
|
|
|
const audioMime = resolveInlineAudioMime(getContent<IFileContent>());
|
|
|
|
|
if (audioMime) {
|
|
|
|
|
const fileContent = getContent<IFileContent>();
|
|
|
|
|
const audioContent = {
|
|
|
|
|
...fileContent,
|
|
|
|
|
info: { ...(fileContent.info ?? {}), mimetype: audioMime },
|
|
|
|
|
} as unknown as IAudioContent;
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<MAudio
|
|
|
|
|
content={audioContent}
|
|
|
|
|
renderAsFile={renderFile}
|
|
|
|
|
renderAudioContent={(props) => (
|
|
|
|
|
<AudioContent {...props} renderMediaControl={(p) => <MediaControl {...p} />} />
|
|
|
|
|
)}
|
|
|
|
|
outlined={outlineAttachment}
|
|
|
|
|
/>
|
|
|
|
|
{renderCaption()}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-05-31 19:49:46 +05:30
|
|
|
return renderFile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === MsgType.Location) {
|
|
|
|
|
return <MLocation content={getContent()} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (msgType === 'm.bad.encrypted') {
|
|
|
|
|
return <MBadEncrypted />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 11:15:49 -04:00
|
|
|
if (msgType === 'm.key.verification.request') {
|
|
|
|
|
return <VerificationRequestContent />;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 19:49:46 +05:30
|
|
|
return <UnsupportedContent />;
|
|
|
|
|
}
|