Enable support for image, video and audio files

pull/21833/head
Jaiwanth 2021-05-31 21:01:19 +05:30
parent 409213ceb4
commit 59c1b67b7d
8 changed files with 86 additions and 21 deletions

View File

@ -34,6 +34,7 @@ export default class MAudioBody extends React.Component {
error: null, error: null,
}; };
} }
onPlayToggle() { onPlayToggle() {
this.setState({ this.setState({
playing: !this.state.playing, playing: !this.state.playing,
@ -41,6 +42,7 @@ export default class MAudioBody extends React.Component {
} }
_getContentUrl() { _getContentUrl() {
if (this.props.mediaSrc) return this.props.mediaSrc;
const media = mediaFromContent(this.props.mxEvent.getContent()); const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) { if (media.isEncrypted) {
return this.state.decryptedUrl; return this.state.decryptedUrl;
@ -49,6 +51,11 @@ export default class MAudioBody extends React.Component {
} }
} }
getFileBody() {
if (this.props.mediaSrc) return null;
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
}
componentDidMount() { componentDidMount() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
@ -101,11 +108,11 @@ export default class MAudioBody extends React.Component {
} }
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const fileBody = this.getFileBody();
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<audio src={contentUrl} controls /> <audio src={contentUrl} controls />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} /> { fileBody }
</span> </span>
); );
} }

View File

@ -242,7 +242,7 @@ export default class MFileBody extends React.Component {
<span className="mx_MFileBody"> <span className="mx_MFileBody">
{placeholder} {placeholder}
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<div style={{display: "none"}}> <div style={{ display: "none" }}>
{ /* { /*
* Add dummy copy of the "a" tag * Add dummy copy of the "a" tag
* We'll use it to learn how the download link * We'll use it to learn how the download link
@ -309,7 +309,7 @@ export default class MFileBody extends React.Component {
if (this.props.tileShape === "file_grid") { if (this.props.tileShape === "file_grid") {
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
{placeholder} { placeholder }
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<a className="mx_MFileBody_downloadLink" {...downloadProps}> <a className="mx_MFileBody_downloadLink" {...downloadProps}>
{ fileName } { fileName }
@ -323,7 +323,7 @@ export default class MFileBody extends React.Component {
} else { } else {
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
{placeholder} { placeholder }
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<a {...downloadProps}> <a {...downloadProps}>
<span className="mx_MFileBody_download_icon" /> <span className="mx_MFileBody_download_icon" />
@ -336,7 +336,7 @@ export default class MFileBody extends React.Component {
} else { } else {
const extra = text ? (': ' + text) : ''; const extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody"> return <span className="mx_MFileBody">
{placeholder} { placeholder }
{ _t("Invalid file%(extra)s", { extra: extra }) } { _t("Invalid file%(extra)s", { extra: extra }) }
</span>; </span>;
} }

View File

@ -90,7 +90,7 @@ export default class MImageBody extends React.Component {
showImage() { showImage() {
localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true");
this.setState({showImage: true}); this.setState({ showImage: true });
this._downloadImage(); this._downloadImage();
} }
@ -172,6 +172,7 @@ export default class MImageBody extends React.Component {
} }
_getContentUrl() { _getContentUrl() {
if (this.props.mediaSrc) return this.props.mediaSrc;
const media = mediaFromContent(this.props.mxEvent.getContent()); const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) { if (media.isEncrypted) {
return this.state.decryptedUrl; return this.state.decryptedUrl;
@ -296,7 +297,7 @@ export default class MImageBody extends React.Component {
if (showImage) { if (showImage) {
// Don't download anything becaue we don't want to display anything. // Don't download anything becaue we don't want to display anything.
this._downloadImage(); this._downloadImage();
this.setState({showImage: true}); this.setState({ showImage: true });
} }
this._afterComponentDidMount(); this._afterComponentDidMount();
@ -345,7 +346,7 @@ export default class MImageBody extends React.Component {
imageElement = <HiddenImagePlaceholder />; imageElement = <HiddenImagePlaceholder />;
} else { } else {
imageElement = ( imageElement = (
<img style={{display: 'none'}} src={thumbUrl} ref={this._image} <img style={{ display: 'none' }} src={thumbUrl} ref={this._image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
onLoad={this.onImageLoad} onLoad={this.onImageLoad}
@ -449,6 +450,7 @@ export default class MImageBody extends React.Component {
// Overidden by MStickerBody // Overidden by MStickerBody
getFileBody() { getFileBody() {
if (this.props.mediaSrc) return null;
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />; return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
} }
@ -466,7 +468,7 @@ export default class MImageBody extends React.Component {
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
let thumbUrl; let thumbUrl;
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { if ((this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) || this.props.mediaSrc) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this._getThumbUrl(); thumbUrl = this._getThumbUrl();

View File

@ -29,6 +29,8 @@ interface IProps {
mxEvent: any; mxEvent: any;
/* called when the video has loaded */ /* called when the video has loaded */
onHeightChanged: () => void; onHeightChanged: () => void;
/* used to refer to the local file while exporting */
mediaSrc?: string;
} }
interface IState { interface IState {
@ -76,6 +78,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
} }
private getContentUrl(): string|null { private getContentUrl(): string|null {
if (this.props.mediaSrc) return this.props.mediaSrc;
const media = mediaFromContent(this.props.mxEvent.getContent()); const media = mediaFromContent(this.props.mxEvent.getContent());
if (media.isEncrypted) { if (media.isEncrypted) {
return this.state.decryptedUrl; return this.state.decryptedUrl;
@ -90,6 +93,9 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
} }
private getThumbUrl(): string|null { private getThumbUrl(): string|null {
// there's no need of thumbnail when the content is local
if (this.props.mediaSrc) return null;
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const media = mediaFromContent(content); const media = mediaFromContent(content);
if (media.isEncrypted) { if (media.isEncrypted) {
@ -184,6 +190,11 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
this.props.onHeightChanged(); this.props.onHeightChanged();
} }
private getFileBody = () => {
if (this.props.mediaSrc) return null;
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />;
}
render() { render() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const autoplay = SettingsStore.getValue("autoplayGifsAndVideos"); const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
@ -197,7 +208,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
); );
} }
// Important: If we aren't autoplaying and we haven't decrypred it yet, show a video with a poster. // Important: If we aren't autoplaying and we haven't decrypted it yet, show a video with a poster.
if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) { if (content.file !== undefined && this.state.decryptedUrl === null && autoplay) {
// Need to decrypt the attachment // Need to decrypt the attachment
// The attachment is decrypted in componentDidMount. // The attachment is decrypted in componentDidMount.
@ -229,6 +240,8 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
preload = "none"; preload = "none";
} }
} }
const fileBody = this.getFileBody();
return ( return (
<span className="mx_MVideoBody"> <span className="mx_MVideoBody">
<video <video
@ -246,7 +259,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
onPlay={this.videoOnPlay} onPlay={this.videoOnPlay}
> >
</video> </video>
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} /> { fileBody }
</span> </span>
); );
} }

View File

@ -23,6 +23,7 @@ import MVoiceMessageBody from "./MVoiceMessageBody";
interface IProps { interface IProps {
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
mediaSrc?: string;
} }
@replaceableComponent("views.messages.MVoiceOrAudioBody") @replaceableComponent("views.messages.MVoiceOrAudioBody")
@ -30,7 +31,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
public render() { public render() {
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
if (isVoiceMessage && voiceMessagesEnabled) { if (isVoiceMessage && voiceMessagesEnabled && !this.props.mediaSrc) {
return <MVoiceMessageBody {...this.props} />; return <MVoiceMessageBody {...this.props} />;
} else { } else {
return <MAudioBody {...this.props} />; return <MAudioBody {...this.props} />;

View File

@ -44,6 +44,9 @@ export default class MessageEvent extends React.Component {
/* the shape of the tile, used */ /* the shape of the tile, used */
tileShape: PropTypes.string, tileShape: PropTypes.string,
/* to set source to local file path during export */
mediaSrc: PropTypes.string,
/* the maximum image height to use, if the event is an image */ /* the maximum image height to use, if the event is an image */
maxImageHeight: PropTypes.number, maxImageHeight: PropTypes.number,
@ -120,6 +123,7 @@ export default class MessageEvent extends React.Component {
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
mediaSrc={this.props.mediaSrc}
maxImageHeight={this.props.maxImageHeight} maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
editState={this.props.editState} editState={this.props.editState}

View File

@ -103,7 +103,7 @@ for (const evType of ALL_RULE_TYPES) {
stateEventTileTypes[evType] = 'messages.TextualEvent'; stateEventTileTypes[evType] = 'messages.TextualEvent';
} }
export function getHandlerTile(ev) { export function getHandlerTile(ev: MatrixEvent) {
const type = ev.getType(); const type = ev.getType();
// don't show verification requests we're not involved in, // don't show verification requests we're not involved in,
@ -251,6 +251,9 @@ interface IProps {
isExporting?: boolean; isExporting?: boolean;
// Used while exporting to refer to the local source rather than the online one
mediaSrc?: string;
// show twelve hour timestamps // show twelve hour timestamps
isTwelveHour?: boolean; isTwelveHour?: boolean;
@ -342,7 +345,7 @@ export default class EventTile extends React.Component<IProps, IState> {
* or 'sent' receipt, for example. * or 'sent' receipt, for example.
* @returns {boolean} * @returns {boolean}
*/ */
private get isEligibleForSpecialReceipt() { private get isEligibleForSpecialReceipt(): boolean {
// First, if there are other read receipts then just short-circuit this. // First, if there are other read receipts then just short-circuit this.
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
if (!this.props.mxEvent) return false; if (!this.props.mxEvent) return false;
@ -1150,6 +1153,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
editState={this.props.editState} editState={this.props.editState}
mediaSrc={this.props.mediaSrc}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
@ -1332,8 +1336,8 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
return <span className="mx_EventTile_readAvatars"> return <span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}> <span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge} { nonCssBadge }
{tooltip} { tooltip }
</span> </span>
</span>; </span>;
} }

View File

@ -20,11 +20,13 @@ import exportJS from "./exportJS";
export default class HTMLExporter extends Exporter { export default class HTMLExporter extends Exporter {
protected zip: JSZip; protected zip: JSZip;
protected avatars: Map<string, boolean>; protected avatars: Map<string, boolean>;
protected permalinkCreator: RoomPermalinkCreator;
constructor(res: MatrixEvent[], room: Room) { constructor(res: MatrixEvent[], room: Room) {
super(res, room); super(res, room);
this.zip = new JSZip(); this.zip = new JSZip();
this.avatars = new Map<string, boolean>(); this.avatars = new Map<string, boolean>();
this.permalinkCreator = new RoomPermalinkCreator(this.room);
} }
protected wrapHTML(content: string, room: Room) { protected wrapHTML(content: string, room: Room) {
@ -152,11 +154,12 @@ export default class HTMLExporter extends Exporter {
return wantsDateSeparator(prevEvent.getDate(), event.getDate()); return wantsDateSeparator(prevEvent.getDate(), event.getDate());
} }
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
const eventTile = <li id={mxEv.getId()}> protected getEventTile(mxEv: MatrixEvent, continuation: boolean, mediaSrc?: string) {
return <li id={mxEv.getId()}>
<EventTile <EventTile
mxEvent={mxEv} mxEvent={mxEv}
continuation={joined} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()} replacingEventId={mxEv.replacingEventId()}
isExporting={true} isExporting={true}
@ -166,8 +169,9 @@ export default class HTMLExporter extends Exporter {
checkUnmounting={() => false} checkUnmounting={() => false}
isTwelveHour={false} isTwelveHour={false}
last={false} last={false}
mediaSrc={mediaSrc}
lastInSection={false} lastInSection={false}
permalinkCreator={new RoomPermalinkCreator(this.room)} permalinkCreator={this.permalinkCreator}
lastSuccessful={false} lastSuccessful={false}
isSelectedEvent={false} isSelectedEvent={false}
getRelationsForEvent={null} getRelationsForEvent={null}
@ -177,6 +181,36 @@ export default class HTMLExporter extends Exporter {
showReadReceipts={false} showReadReceipts={false}
/> />
</li> </li>
}
protected async createMessageBody(mxEv: MatrixEvent, joined = false) {
let eventTile: JSX.Element;
switch (mxEv.getContent().msgtype) {
case "m.image": {
const blob = await this.getMediaBlob(mxEv);
const filePath = `images/${mxEv.getId()}.${blob.type.replace("image/", "")}`;
eventTile = this.getEventTile(mxEv, joined, filePath);
this.zip.file(filePath, blob);
break;
}
case "m.video": {
const blob = await this.getMediaBlob(mxEv);
const filePath = `videos/${mxEv.getId()}.${blob.type.replace("video/", "")}`;
eventTile = this.getEventTile(mxEv, joined, filePath);
this.zip.file(filePath, blob);
break;
}
case "m.audio": {
const blob = await this.getMediaBlob(mxEv);
const filePath = `audio/${mxEv.getId()}.${blob.type.replace("audio/", "")}`;
eventTile = this.getEventTile(mxEv, joined, filePath);
this.zip.file(filePath, blob);
break;
}
default:
eventTile = this.getEventTile(mxEv, joined);
break;
}
return renderToStaticMarkup(eventTile); return renderToStaticMarkup(eventTile);
} }