657ca3a5ca
P3-5: Giphy/Tenor URL preview cards — full-width thumbnail from og:image mxc URL, GIF badge overlay, site badge + title footer; GifCard shared by both; BadgeGiphy (teal) and BadgeTenor (blue) CSS classes P3-9: Policy list viewer — read-only panel in Room Settings + Space Settings (admin/50+ PL only); enter room ID or alias; tabs for Users / Rooms / Servers; glob pattern warning color; Ban badge; entity + reason P5-8: Mention highlight pulse — 0.6s scale+glow keyframe on incoming @mention messages; prefers-reduced-motion aware; only fires on new incoming messages (isNewRef), not on history load; onAnimationEnd cleanup P5-19: Collapsible long messages — ResizeObserver clamps text bodies >320px with gradient fade + "Read more ↓" / "Show less ↑" button; resets on eventId change; skips images/video/audio/file; smooth CSS transition P5-23: Message send animation — own messages fade+scale in (0.97→1, 0.4→1 opacity, 150ms ease-out); prefers-reduced-motion aware; one-shot via isNewRef + onAnimationEnd clear P5-26: Room context menu — Copy Link (matrix.to URL, 1.5s Copied! feedback), Mute with duration (15m/1h/8h/24h/indefinite, localStorage timer key io.lotus.mute_timers), Mark as read; Icons.Link + Icons.BellMute BUG D&D: dragCounter ref replaces fragile dragState machine — enter increments, leave decrements (hides at 0), drop resets to 0; fixes spurious dragleave from child element boundary crossings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
7.2 KiB
TypeScript
285 lines
7.2 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,
|
|
VerificationRequestContent,
|
|
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';
|
|
|
|
type RenderMessageContentProps = {
|
|
displayName: string;
|
|
msgType: string;
|
|
ts: number;
|
|
edited?: boolean;
|
|
onEditHistoryClick?: () => void;
|
|
getContent: <T>() => T;
|
|
mediaAutoLoad?: boolean;
|
|
urlPreview?: boolean;
|
|
highlightRegex?: RegExp;
|
|
htmlReactParserOptions: HTMLReactParserOptions;
|
|
linkifyOpts: Opts;
|
|
outlineAttachment?: boolean;
|
|
eventId?: string;
|
|
};
|
|
export function RenderMessageContent({
|
|
displayName,
|
|
msgType,
|
|
ts,
|
|
edited,
|
|
onEditHistoryClick,
|
|
getContent,
|
|
mediaAutoLoad,
|
|
urlPreview,
|
|
highlightRegex,
|
|
htmlReactParserOptions,
|
|
linkifyOpts,
|
|
outlineAttachment,
|
|
eventId,
|
|
}: RenderMessageContentProps) {
|
|
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) {
|
|
return (
|
|
<MText
|
|
style={{ marginTop: config.space.S200 }}
|
|
edited={edited}
|
|
onEditHistoryClick={onEditHistoryClick}
|
|
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()}
|
|
</>
|
|
);
|
|
|
|
if (msgType === MsgType.Text) {
|
|
return (
|
|
<MText
|
|
edited={edited}
|
|
onEditHistoryClick={onEditHistoryClick}
|
|
content={getContent()}
|
|
renderBody={(props) => (
|
|
<RenderBody
|
|
{...props}
|
|
highlightRegex={highlightRegex}
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
|
linkifyOpts={linkifyOpts}
|
|
/>
|
|
)}
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
|
eventId={eventId}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (msgType === MsgType.Emote) {
|
|
return (
|
|
<MEmote
|
|
displayName={displayName}
|
|
edited={edited}
|
|
onEditHistoryClick={onEditHistoryClick}
|
|
content={getContent()}
|
|
renderBody={(props) => (
|
|
<RenderBody
|
|
{...props}
|
|
highlightRegex={highlightRegex}
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
|
linkifyOpts={linkifyOpts}
|
|
/>
|
|
)}
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
|
eventId={eventId}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (msgType === MsgType.Notice) {
|
|
return (
|
|
<MNotice
|
|
edited={edited}
|
|
onEditHistoryClick={onEditHistoryClick}
|
|
content={getContent()}
|
|
renderBody={(props) => (
|
|
<RenderBody
|
|
{...props}
|
|
highlightRegex={highlightRegex}
|
|
htmlReactParserOptions={htmlReactParserOptions}
|
|
linkifyOpts={linkifyOpts}
|
|
/>
|
|
)}
|
|
renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined}
|
|
eventId={eventId}
|
|
/>
|
|
);
|
|
}
|
|
|
|
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, ...props }) => (
|
|
<VideoContent
|
|
body={body}
|
|
info={info}
|
|
{...props}
|
|
renderThumbnail={
|
|
mediaAutoLoad
|
|
? () => (
|
|
<ThumbnailContent
|
|
info={info}
|
|
renderImage={(src) => (
|
|
<Image alt={body} title={body} src={src} loading="lazy" />
|
|
)}
|
|
/>
|
|
)
|
|
: undefined
|
|
}
|
|
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 />;
|
|
}
|
|
|
|
if (msgType === 'm.key.verification.request') {
|
|
return <VerificationRequestContent />;
|
|
}
|
|
|
|
return <UnsupportedContent />;
|
|
}
|