From 59c1b67b7dcebb0adab9065e09d6bc0dc0497c15 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Mon, 31 May 2021 21:01:19 +0530 Subject: [PATCH] Enable support for image, video and audio files --- src/components/views/messages/MAudioBody.js | 11 ++++- src/components/views/messages/MFileBody.js | 8 ++-- src/components/views/messages/MImageBody.js | 10 +++-- src/components/views/messages/MVideoBody.tsx | 17 +++++++- .../views/messages/MVoiceOrAudioBody.tsx | 3 +- src/components/views/messages/MessageEvent.js | 4 ++ src/components/views/rooms/EventTile.tsx | 12 ++++-- src/utils/exportUtils/HtmlExport.tsx | 42 +++++++++++++++++-- 8 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 0d5e449fc0..53aa013503 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -34,6 +34,7 @@ export default class MAudioBody extends React.Component { error: null, }; } + onPlayToggle() { this.setState({ playing: !this.state.playing, @@ -41,6 +42,7 @@ export default class MAudioBody extends React.Component { } _getContentUrl() { + if (this.props.mediaSrc) return this.props.mediaSrc; const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { return this.state.decryptedUrl; @@ -49,6 +51,11 @@ export default class MAudioBody extends React.Component { } } + getFileBody() { + if (this.props.mediaSrc) return null; + return ; + } + componentDidMount() { const content = this.props.mxEvent.getContent(); if (content.file !== undefined && this.state.decryptedUrl === null) { @@ -101,11 +108,11 @@ export default class MAudioBody extends React.Component { } const contentUrl = this._getContentUrl(); - + const fileBody = this.getFileBody(); return ( ); } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 8f464e08bd..9dc1de7683 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -242,7 +242,7 @@ export default class MFileBody extends React.Component { {placeholder}
-
+
{ /* * Add dummy copy of the "a" tag * 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") { return ( - {placeholder} + { placeholder }
{ fileName } @@ -323,7 +323,7 @@ export default class MFileBody extends React.Component { } else { return ( - {placeholder} + { placeholder }
@@ -336,7 +336,7 @@ export default class MFileBody extends React.Component { } else { const extra = text ? (': ' + text) : ''; return - {placeholder} + { placeholder } { _t("Invalid file%(extra)s", { extra: extra }) } ; } diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 6505b1d66a..31cdeed6a0 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -90,7 +90,7 @@ export default class MImageBody extends React.Component { showImage() { localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); - this.setState({showImage: true}); + this.setState({ showImage: true }); this._downloadImage(); } @@ -172,6 +172,7 @@ export default class MImageBody extends React.Component { } _getContentUrl() { + if (this.props.mediaSrc) return this.props.mediaSrc; const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { return this.state.decryptedUrl; @@ -296,7 +297,7 @@ export default class MImageBody extends React.Component { if (showImage) { // Don't download anything becaue we don't want to display anything. this._downloadImage(); - this.setState({showImage: true}); + this.setState({ showImage: true }); } this._afterComponentDidMount(); @@ -345,7 +346,7 @@ export default class MImageBody extends React.Component { imageElement = ; } else { imageElement = ( - {content.body}; } @@ -466,7 +468,7 @@ export default class MImageBody extends React.Component { const contentUrl = this._getContentUrl(); let thumbUrl; - if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { + if ((this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) || this.props.mediaSrc) { thumbUrl = contentUrl; } else { thumbUrl = this._getThumbUrl(); diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 2efdce506e..68cbb6702c 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -29,6 +29,8 @@ interface IProps { mxEvent: any; /* called when the video has loaded */ onHeightChanged: () => void; + /* used to refer to the local file while exporting */ + mediaSrc?: string; } interface IState { @@ -76,6 +78,7 @@ export default class MVideoBody extends React.PureComponent { } private getContentUrl(): string|null { + if (this.props.mediaSrc) return this.props.mediaSrc; const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { return this.state.decryptedUrl; @@ -90,6 +93,9 @@ export default class MVideoBody extends React.PureComponent { } 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 media = mediaFromContent(content); if (media.isEncrypted) { @@ -184,6 +190,11 @@ export default class MVideoBody extends React.PureComponent { this.props.onHeightChanged(); } + private getFileBody = () => { + if (this.props.mediaSrc) return null; + return ; + } + render() { const content = this.props.mxEvent.getContent(); const autoplay = SettingsStore.getValue("autoplayGifsAndVideos"); @@ -197,7 +208,7 @@ export default class MVideoBody extends React.PureComponent { ); } - // 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) { // Need to decrypt the attachment // The attachment is decrypted in componentDidMount. @@ -229,6 +240,8 @@ export default class MVideoBody extends React.PureComponent { preload = "none"; } } + + const fileBody = this.getFileBody(); return ( - + { fileBody } ); } diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx index 0cebcf3440..d548d8d2fa 100644 --- a/src/components/views/messages/MVoiceOrAudioBody.tsx +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -23,6 +23,7 @@ import MVoiceMessageBody from "./MVoiceMessageBody"; interface IProps { mxEvent: MatrixEvent; + mediaSrc?: string; } @replaceableComponent("views.messages.MVoiceOrAudioBody") @@ -30,7 +31,7 @@ export default class MVoiceOrAudioBody extends React.PureComponent { public render() { const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); - if (isVoiceMessage && voiceMessagesEnabled) { + if (isVoiceMessage && voiceMessagesEnabled && !this.props.mediaSrc) { return ; } else { return ; diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 78e0dc422d..84a3d56d77 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -44,6 +44,9 @@ export default class MessageEvent extends React.Component { /* the shape of the tile, used */ 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 */ maxImageHeight: PropTypes.number, @@ -120,6 +123,7 @@ export default class MessageEvent extends React.Component { highlightLink={this.props.highlightLink} showUrlPreview={this.props.showUrlPreview} tileShape={this.props.tileShape} + mediaSrc={this.props.mediaSrc} maxImageHeight={this.props.maxImageHeight} replacingEventId={this.props.replacingEventId} editState={this.props.editState} diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index ff10b3255a..8356119d71 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -103,7 +103,7 @@ for (const evType of ALL_RULE_TYPES) { stateEventTileTypes[evType] = 'messages.TextualEvent'; } -export function getHandlerTile(ev) { +export function getHandlerTile(ev: MatrixEvent) { const type = ev.getType(); // don't show verification requests we're not involved in, @@ -251,6 +251,9 @@ interface IProps { isExporting?: boolean; + // Used while exporting to refer to the local source rather than the online one + mediaSrc?: string; + // show twelve hour timestamps isTwelveHour?: boolean; @@ -342,7 +345,7 @@ export default class EventTile extends React.Component { * or 'sent' receipt, for example. * @returns {boolean} */ - private get isEligibleForSpecialReceipt() { + private get isEligibleForSpecialReceipt(): boolean { // 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.mxEvent) return false; @@ -1150,6 +1153,7 @@ export default class EventTile extends React.Component { mxEvent={this.props.mxEvent} replacingEventId={this.props.replacingEventId} editState={this.props.editState} + mediaSrc={this.props.mediaSrc} highlights={this.props.highlights} highlightLink={this.props.highlightLink} showUrlPreview={this.props.showUrlPreview} @@ -1332,8 +1336,8 @@ class SentReceipt extends React.PureComponent - {nonCssBadge} - {tooltip} + { nonCssBadge } + { tooltip } ; } diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 6ce7ad5c94..dfdd8a32cd 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -20,11 +20,13 @@ import exportJS from "./exportJS"; export default class HTMLExporter extends Exporter { protected zip: JSZip; protected avatars: Map; + protected permalinkCreator: RoomPermalinkCreator; constructor(res: MatrixEvent[], room: Room) { super(res, room); this.zip = new JSZip(); this.avatars = new Map(); + this.permalinkCreator = new RoomPermalinkCreator(this.room); } protected wrapHTML(content: string, room: Room) { @@ -152,11 +154,12 @@ export default class HTMLExporter extends Exporter { return wantsDateSeparator(prevEvent.getDate(), event.getDate()); } - protected async createMessageBody(mxEv: MatrixEvent, joined = false) { - const eventTile =
  • + + protected getEventTile(mxEv: MatrixEvent, continuation: boolean, mediaSrc?: string) { + return
  • false} isTwelveHour={false} last={false} + mediaSrc={mediaSrc} lastInSection={false} - permalinkCreator={new RoomPermalinkCreator(this.room)} + permalinkCreator={this.permalinkCreator} lastSuccessful={false} isSelectedEvent={false} getRelationsForEvent={null} @@ -177,6 +181,36 @@ export default class HTMLExporter extends Exporter { showReadReceipts={false} />
  • + } + + 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); }