vojo/src/app/components/RenderMessageContent.tsx

337 lines
9.3 KiB
TypeScript

import React, { MouseEventHandler, createContext, useContext } 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,
RenderImageContentProps,
RenderVideoContentProps,
StreamMediaCaption,
StreamMediaImage,
StreamMediaVideo,
ThumbnailContent,
UnsupportedContent,
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';
import { testMatrixTo } from '../plugins/matrix-to';
import { IImageContent } from '../../types/matrix/common';
import { logMedia } from './message/attachment/streamMediaDebug';
// Threads the StreamLayout's mediaMode info from Message.tsx down to the
// image / video rendering branches below. Non-null only for media messages
// in the timeline; pin-menu / message-search leave it null and fall back
// to the legacy MImage / MVideo Attachment chrome.
export type StreamMediaContextValue = {
own: boolean;
username: string;
senderId: string;
onUsernameClick: MouseEventHandler<HTMLButtonElement>;
onUsernameContextMenu: MouseEventHandler<HTMLButtonElement>;
};
export const StreamMediaContext = createContext<StreamMediaContextValue | null>(null);
export const useStreamMediaContext = (): StreamMediaContextValue | null =>
useContext(StreamMediaContext);
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;
};
export function RenderMessageContent({
displayName,
msgType,
ts,
edited,
getContent,
mediaAutoLoad,
urlPreview,
highlightRegex,
htmlReactParserOptions,
linkifyOpts,
outlineAttachment,
}: RenderMessageContentProps) {
const streamMedia = useStreamMediaContext();
const renderUrlsPreview = (urls: string[]) => {
const filteredUrls = urls.filter((url) => !testMatrixTo(url));
if (filteredUrls.length === 0) return undefined;
return (
<UrlPreviewHolder>
{filteredUrls.map((url) => (
<UrlPreviewCard key={url} url={url} ts={ts} />
))}
</UrlPreviewHolder>
);
};
const renderCaption = () => {
const content: IImageContent = getContent();
if (content.filename && content.filename !== content.body) {
const captionNode = (
<MText
style={streamMedia ? undefined : { marginTop: config.space.S200 }}
edited={edited}
content={content}
renderBody={(props) => (
<RenderBody
{...props}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
/>
)}
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
/>
);
if (streamMedia) {
return <div className={StreamMediaCaption}>{captionNode}</div>;
}
return captionNode;
}
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()}
</>
);
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) {
logMedia('RenderMessageContent', {
msgType,
streamMediaPresent: !!streamMedia,
branch: streamMedia ? 'StreamMediaImage' : 'MImage(legacy)',
});
const renderImageInside = (props: RenderImageContentProps) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => <Image {...p} loading="lazy" decoding="async" />}
renderViewer={(p) => <ImageViewer {...p} />}
/>
);
return (
<>
{streamMedia ? (
<StreamMediaImage
content={getContent()}
own={streamMedia.own}
overlay={streamMedia.username}
senderId={streamMedia.senderId}
onUsernameClick={streamMedia.onUsernameClick}
onUsernameContextMenu={streamMedia.onUsernameContextMenu}
renderImageContent={renderImageInside}
/>
) : (
<MImage
content={getContent()}
renderImageContent={renderImageInside}
outlined={outlineAttachment}
/>
)}
{renderCaption()}
</>
);
}
if (msgType === MsgType.Video) {
logMedia('RenderMessageContent', {
msgType,
streamMediaPresent: !!streamMedia,
branch: streamMedia ? 'StreamMediaVideo' : 'MVideo(legacy)',
});
const renderVideoInside = ({ body, info, ...props }: RenderVideoContentProps) => (
<VideoContent
body={body}
info={info}
{...props}
renderThumbnail={
mediaAutoLoad
? () => (
<ThumbnailContent
info={info}
renderImage={(src) => (
<Image alt={body} title={body} src={src} loading="lazy" decoding="async" />
)}
/>
)
: undefined
}
renderVideo={(p) => <Video {...p} />}
/>
);
return (
<>
{streamMedia ? (
<StreamMediaVideo
content={getContent()}
own={streamMedia.own}
overlay={streamMedia.username}
senderId={streamMedia.senderId}
onUsernameClick={streamMedia.onUsernameClick}
onUsernameContextMenu={streamMedia.onUsernameContextMenu}
renderAsFile={renderFile}
renderVideoContent={renderVideoInside}
/>
) : (
<MVideo
content={getContent()}
renderAsFile={renderFile}
renderVideoContent={renderVideoInside}
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) {
const fileMime = (getContent() as { info?: { mimetype?: string } }).info?.mimetype;
logMedia('RenderMessageContent', {
msgType,
branch: 'MFile(legacy)',
fileMime,
});
return renderFile();
}
if (msgType === MsgType.Location) {
return <MLocation content={getContent()} />;
}
if (msgType === 'm.bad.encrypted') {
return <MBadEncrypted />;
}
return <UnsupportedContent />;
}