From af5cfff51db48c4a96c5393a25c76675839e864d Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 4 Mar 2021 23:17:29 +0200 Subject: [PATCH 01/49] feat: edit button on View Source dialog reuse component SendCustomEvent swap it in place in the View Source dialog the Back button takes you to the View Source dialog, not the DevTools dialog do not display the flip toggle box for changing between State Event and Normal Event --- src/components/structures/ViewSource.js | 180 ++++++++++++++---- .../views/context_menus/MessageContextMenu.js | 14 +- .../views/dialogs/DevtoolsDialog.js | 7 +- 3 files changed, 152 insertions(+), 49 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index ca6c0d4226..7fe862cff5 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -16,12 +16,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import SyntaxHighlight from '../views/elements/SyntaxHighlight'; -import {_t} from "../../languageHandler"; +import React from "react"; +import PropTypes from "prop-types"; +import SyntaxHighlight from "../views/elements/SyntaxHighlight"; +import { _t } from "../../languageHandler"; import * as sdk from "../../index"; - +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; export default class ViewSource extends React.Component { static propTypes = { @@ -31,48 +32,157 @@ export default class ViewSource extends React.Component { eventId: PropTypes.string.isRequired, isEncrypted: PropTypes.bool.isRequired, decryptedContent: PropTypes.object, + event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu }; + constructor(props) { + super(props); + + this.state = { + editComponent: null, + }; + } + + onBack() { + this.setState({ editComponent: null }); + } + + editEvent() { + const isStateEvent = this.props.event.isState(); + console.log("isStateEvent", isStateEvent); + if (isStateEvent) { + this.setState({ + editComponent: ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: this.props.event.getType(), + evContent: JSON.stringify( + this.props.event.getContent(), + null, + "\t" + ), + stateKey: this.props.event.getStateKey(), + }} + /> + )} + + ), + }); + } else { + // send an edit-message event + // prefill the "m.new_content" field + const originalContent = this.props.event.getContent(); + const originalEventId = this.props.eventId; + const content = { + ...originalContent, + "m.new_content": originalContent, + "m.relates_to": { + rel_type: "m.replace", + event_id: originalEventId, + }, + }; + this.setState({ + editComponent: ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: this.props.event.getType(), + evContent: JSON.stringify( + content, + null, + "\t" + ), + }} + /> + )} + + ), + }); + } + } + render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); let content; if (this.props.isEncrypted) { - content = <> -
- - {_t("Decrypted event source")} - - - { JSON.stringify(this.props.decryptedContent, null, 2) } - -
-
- - {_t("Original event source")} - - - { JSON.stringify(this.props.content, null, 2) } - -
- ; + content = ( + <> +
+ + + {_t("Decrypted event source")} + + + + {JSON.stringify( + this.props.decryptedContent, + null, + 2 + )} + +
+
+ + + {_t("Original event source")} + + + + {JSON.stringify(this.props.content, null, 2)} + +
+ + ); } else { - content = <> -
{_t("Original event source")}
- - { JSON.stringify(this.props.content, null, 2) } - - ; + content = ( + <> +
+ {_t("Original event source")} +
+ + {JSON.stringify(this.props.content, null, 2)} + + + ); } + const isEditing = this.state.editComponent !== null; + console.log(isEditing); + return ( - -
-
Room ID: { this.props.roomId }
-
Event ID: { this.props.eventId }
+ +
+
+ Room ID: {this.props.roomId} +
+
+ Event ID: {this.props.eventId} +
- { content } + {isEditing ? this.state.editComponent : content}
+ {!isEditing && ( +
+ +
+ )} ); } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index b002d1ec62..a1c111b19c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -130,20 +130,10 @@ export default class MessageContextMenu extends React.Component { roomId: ev.getRoomId(), eventId: ev.getId(), content: ev.event, + event: ev, isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - decryptedContent: ev._clearEvent, - }, 'mx_Dialog_viewsource'); - this.closeMenu(); - }; - - onViewClearSourceClick = () => { - const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; - const ViewSource = sdk.getComponent('structures.ViewSource'); - Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { - roomId: ev.getRoomId(), - eventId: ev.getId(), // FIXME: _clearEvent is private - content: ev._clearEvent, + decryptedContent: ev._clearEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 814378bb51..5d571461fc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -73,13 +73,14 @@ class GenericEditor extends React.PureComponent { } } -class SendCustomEvent extends GenericEditor { +export class SendCustomEvent extends GenericEditor { static getLabel() { return _t('Send Custom Event'); } static propTypes = { onBack: PropTypes.func.isRequired, room: PropTypes.instanceOf(Room).isRequired, forceStateEvent: PropTypes.bool, + forceGeneralEvent: PropTypes.bool, inputs: PropTypes.object, }; @@ -140,6 +141,8 @@ class SendCustomEvent extends GenericEditor {
; } + const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent; + return
@@ -155,7 +158,7 @@ class SendCustomEvent extends GenericEditor {
{ !this.state.message && } - { !this.state.message && !this.props.forceStateEvent &&
+ { showTglFlip &&
} From 288d98daede9f1ea6b1045e6de930c61f5f14c0e Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Fri, 5 Mar 2021 00:07:59 +0200 Subject: [PATCH 02/49] chore: format, lint --- src/components/structures/ViewSource.js | 58 ++++--------------- .../views/dialogs/DevtoolsDialog.js | 2 +- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 7fe862cff5..a31876ea76 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -61,11 +61,7 @@ export default class ViewSource extends React.Component { onBack={() => this.onBack()} inputs={{ eventType: this.props.event.getType(), - evContent: JSON.stringify( - this.props.event.getContent(), - null, - "\t" - ), + evContent: JSON.stringify(this.props.event.getContent(), null, "\t"), stateKey: this.props.event.getStateKey(), }} /> @@ -97,11 +93,7 @@ export default class ViewSource extends React.Component { onBack={() => this.onBack()} inputs={{ eventType: this.props.event.getType(), - evContent: JSON.stringify( - content, - null, - "\t" - ), + evContent: JSON.stringify(content, null, "\t"), }} /> )} @@ -120,39 +112,23 @@ export default class ViewSource extends React.Component { <>
- - {_t("Decrypted event source")} - + {_t("Decrypted event source")} - - {JSON.stringify( - this.props.decryptedContent, - null, - 2 - )} - + {JSON.stringify(this.props.decryptedContent, null, 2)}
- - {_t("Original event source")} - + {_t("Original event source")} - - {JSON.stringify(this.props.content, null, 2)} - + {JSON.stringify(this.props.content, null, 2)}
); } else { content = ( <> -
- {_t("Original event source")} -
- - {JSON.stringify(this.props.content, null, 2)} - +
{_t("Original event source")}
+ {JSON.stringify(this.props.content, null, 2)} ); } @@ -161,26 +137,16 @@ export default class ViewSource extends React.Component { console.log(isEditing); return ( - +
-
- Room ID: {this.props.roomId} -
-
- Event ID: {this.props.eventId} -
+
Room ID: {this.props.roomId}
+
Event ID: {this.props.eventId}
{isEditing ? this.state.editComponent : content}
{!isEditing && (
- +
)} diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 5d571461fc..82f2df6534 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -142,7 +142,7 @@ export class SendCustomEvent extends GenericEditor { } const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent; - + return
From 51ac5421c9848be775c977009454e7f55c79d155 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 11:30:31 +0200 Subject: [PATCH 03/49] chore: refactor code pass only the mxEvent object to ViewSource derive the necessary values inside the component --- src/components/structures/ViewSource.js | 161 +++++++++--------- .../views/context_menus/MessageContextMenu.js | 9 +- .../views/messages/EditHistoryMessage.js | 6 +- 3 files changed, 86 insertions(+), 90 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index a31876ea76..369a0a1ddd 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -26,127 +26,134 @@ import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; export default class ViewSource extends React.Component { static propTypes = { - content: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - eventId: PropTypes.string.isRequired, - isEncrypted: PropTypes.bool.isRequired, - decryptedContent: PropTypes.object, - event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu + mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu }; constructor(props) { super(props); this.state = { - editComponent: null, + isEditing: false, }; } onBack() { - this.setState({ editComponent: null }); + this.setState({ isEditing: false }); } - editEvent() { - const isStateEvent = this.props.event.isState(); - console.log("isStateEvent", isStateEvent); - if (isStateEvent) { - this.setState({ - editComponent: ( - - {(cli) => ( - this.onBack()} - inputs={{ - eventType: this.props.event.getType(), - evContent: JSON.stringify(this.props.event.getContent(), null, "\t"), - stateKey: this.props.event.getStateKey(), - }} - /> - )} - - ), - }); - } else { - // send an edit-message event - // prefill the "m.new_content" field - const originalContent = this.props.event.getContent(); - const originalEventId = this.props.eventId; - const content = { - ...originalContent, - "m.new_content": originalContent, - "m.relates_to": { - rel_type: "m.replace", - event_id: originalEventId, - }, - }; - this.setState({ - editComponent: ( - - {(cli) => ( - this.onBack()} - inputs={{ - eventType: this.props.event.getType(), - evContent: JSON.stringify(content, null, "\t"), - }} - /> - )} - - ), - }); - } + onEdit() { + this.setState({ isEditing: true }); } - render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + // returns the dialog body for viewing the event source + viewSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const originalEventSource = mxEvent.event; - let content; - if (this.props.isEncrypted) { - content = ( + if (isEncrypted) { + return ( <>
{_t("Decrypted event source")} - {JSON.stringify(this.props.decryptedContent, null, 2)} + {JSON.stringify(decryptedEventSource, null, 2)}
{_t("Original event source")} - {JSON.stringify(this.props.content, null, 2)} + {JSON.stringify(originalEventSource, null, 2)}
); } else { - content = ( + return ( <>
{_t("Original event source")}
- {JSON.stringify(this.props.content, null, 2)} + {JSON.stringify(originalEventSource, null, 2)} ); } + } - const isEditing = this.state.editComponent !== null; - console.log(isEditing); + // returns the SendCustomEvent component prefilled with the correct details + editSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isStateEvent = mxEvent.isState(); + console.log("isStateEvent", isStateEvent); + const roomId = mxEvent.getRoomId(); + const eventId = mxEvent.getId(); + const originalContent = mxEvent.getContent(); + if (isStateEvent) { + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(originalContent, null, "\t"), + stateKey: mxEvent.getStateKey(), + }} + /> + )} + + ); + } else { + // send an edit-message event + // prefill the "m.new_content" field + const newContent = { + ...originalContent, + "m.new_content": originalContent, + "m.relates_to": { + rel_type: "m.replace", + event_id: eventId, + }, + }; + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(newContent, null, "\t"), + }} + /> + )} + + ); + } + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + + const isEditing = this.state.isEditing; + const roomId = mxEvent.getRoomId(); + const eventId = mxEvent.getId(); return (
-
Room ID: {this.props.roomId}
-
Event ID: {this.props.eventId}
+
Room ID: {roomId}
+
Event ID: {eventId}
- {isEditing ? this.state.editComponent : content} + {isEditing ? this.editSourceContent() : this.viewSourceContent()}
{!isEditing && (
- +
)} diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index a1c111b19c..6809d28e36 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -124,16 +124,9 @@ export default class MessageContextMenu extends React.Component { }; onViewSourceClick = () => { - const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', '', ViewSource, { - roomId: ev.getRoomId(), - eventId: ev.getId(), - content: ev.event, - event: ev, - isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - // FIXME: _clearEvent is private - decryptedContent: ev._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index 68a3c95745..3bd9dfbd21 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -74,11 +74,7 @@ export default class EditHistoryMessage extends React.PureComponent { _onViewSourceClick = () => { const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, { - roomId: this.props.mxEvent.getRoomId(), - eventId: this.props.mxEvent.getId(), - content: this.props.mxEvent.event, - isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - decryptedContent: this.props.mxEvent._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); }; From 29b95e60833fef1bed598897dad2deff4e875ff4 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 16:47:29 +0200 Subject: [PATCH 04/49] fix: make edit prefill work correctly from EditHistory handle encrypted and unencrypted events get the correct event_id (the base message) when called from EditHistoryMessage keep only the `body` and `msgtype` fields when prefilling --- src/components/structures/ViewSource.js | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 369a0a1ddd..4ee70ee2a7 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -39,6 +39,7 @@ export default class ViewSource extends React.Component { } onBack() { + // TODO: refresh the "Event ID:" modal header this.setState({ isEditing: false }); } @@ -80,15 +81,28 @@ export default class ViewSource extends React.Component { } } + // returns the id of the initial message, not the id of the previous edit + getBaseEventId() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const baseMxEvent = this.props.mxEvent; + + if (isEncrypted) { + // `relates_to` field is inside the encrypted event + return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } else { + return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } + } + // returns the SendCustomEvent component prefilled with the correct details editSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isStateEvent = mxEvent.isState(); - console.log("isStateEvent", isStateEvent); const roomId = mxEvent.getRoomId(); - const eventId = mxEvent.getId(); const originalContent = mxEvent.getContent(); + if (isStateEvent) { return ( @@ -107,14 +121,19 @@ export default class ViewSource extends React.Component { ); } else { - // send an edit-message event - // prefill the "m.new_content" field + // prefill an edit-message event + // keep only the `body` and `msgtype` fields of originalContent + const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there const newContent = { - ...originalContent, - "m.new_content": originalContent, + "body": ` * ${bodyToStartFrom}`, + "msgtype": originalContent.msgtype, + "m.new_content": { + body: bodyToStartFrom, + msgtype: originalContent.msgtype, + }, "m.relates_to": { rel_type: "m.replace", - event_id: eventId, + event_id: this.getBaseEventId(), }, }; return ( From df52ec28d60ecdf9719f3b5339a805f6a643f753 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 17:09:46 +0200 Subject: [PATCH 05/49] fix: show edit button only if you have permission --- src/components/structures/ViewSource.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 4ee70ee2a7..ddcffe4f7f 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -23,6 +23,7 @@ import { _t } from "../../languageHandler"; import * as sdk from "../../index"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; +import { canEditContent } from "../../utils/EventUtils"; export default class ViewSource extends React.Component { static propTypes = { @@ -162,6 +163,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); + const canEdit = canEditContent(this.props.mxEvent); return (
@@ -170,7 +172,7 @@ export default class ViewSource extends React.Component {
{isEditing ? this.editSourceContent() : this.viewSourceContent()}
- {!isEditing && ( + {!isEditing && canEdit && (
From 9287e8dfa4f55b368a3c108d3c15442cdfdc4c1c Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Mon, 8 Mar 2021 22:15:34 +0200 Subject: [PATCH 06/49] use isEncrypted, edit state events --- src/components/structures/ViewSource.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index ddcffe4f7f..cfe28e9f73 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -51,7 +51,7 @@ export default class ViewSource extends React.Component { // returns the dialog body for viewing the event source viewSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit - const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const isEncrypted = mxEvent.isEncrypted(); const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private const originalEventSource = mxEvent.event; @@ -85,7 +85,7 @@ export default class ViewSource extends React.Component { // returns the id of the initial message, not the id of the previous edit getBaseEventId() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit - const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const isEncrypted = mxEvent.isEncrypted(); const baseMxEvent = this.props.mxEvent; if (isEncrypted) { @@ -163,7 +163,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); - const canEdit = canEditContent(this.props.mxEvent); + const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState(); return (
From 0936ea7e640ac10449150eaed1615bc99c52e70c Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:46:37 +0200 Subject: [PATCH 07/49] feat: show edit button only when user has permissions call appropriate functions for state events and edit message events --- src/components/structures/ViewSource.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index cfe28e9f73..39666edd65 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -24,6 +24,7 @@ import * as sdk from "../../index"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; import { canEditContent } from "../../utils/EventUtils"; +import { MatrixClientPeg } from '../../MatrixClientPeg'; export default class ViewSource extends React.Component { static propTypes = { @@ -156,6 +157,12 @@ export default class ViewSource extends React.Component { } } + canSendStateEvent(mxEvent) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(mxEvent.getRoomId()); + return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); + } + render() { const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit @@ -163,7 +170,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); - const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState(); + const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); return (
From 681529aa3cb54b630fd02a25613d4f9e7379c536 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:26:35 +0000 Subject: [PATCH 08/49] Upgrade matrix-js-sdk to 9.9.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7ed1b272da..e7fff438ff 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "9.9.0-rc.1", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index f99ea5900d..5c78e70590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5572,9 +5572,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "9.8.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f" +matrix-js-sdk@9.9.0-rc.1: + version "9.9.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.9.0-rc.1.tgz#5ee28aee89a87ccdf742d1512bc44ec54454e94f" + integrity sha512-A3pY5CyCNE5+QdpYL/C7FGU8KjBojXRWbtWsbeTMFwZEuzkIYlCtev1i07NbN3kbF83R3TtOfSCxQITm1C5jwg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 89f561a1ee645873e0a93fe1a6bc8967560ae8ab Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:31:17 +0000 Subject: [PATCH 09/49] Prepare changelog for v3.16.0-rc.1 --- CHANGELOG.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c31eedf93b..d4ffeb5fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,110 @@ +Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) + + * Upgrade to JS SDK 9.9.0-rc.1 + * Translations update from Weblate + [\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743) + * Document behaviour of showReadReceipts=false for sent receipts + [\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739) + * Tweak sent marker code style + [\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741) + * Fix sent markers disappearing for edits/reactions + [\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737) + * Ignore to-device decryption in the room list store + [\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740) + * Spaces suggested rooms support + [\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736) + * Add tooltips to sent/sending receipts + [\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738) + * Remove a bunch of useless 'use strict' definitions + [\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735) + * [SK-1] Fix types for replaceableComponent + [\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732) + * [SK-2] Make debugging skinning problems easier + [\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733) + * Support sending invite reasons with /invite command + [\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695) + * Fix clicking on the avatar for opening member info requires pixel-perfect + accuracy + [\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717) + * Display decrypted and encrypted event source on the same dialog + [\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713) + * Fix units of TURN server expiry time + [\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730) + * Display room name in pills instead of address + [\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624) + * Refresh UI for file uploads + [\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723) + * UI refresh for uploaded files + [\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719) + * Improve message sending states to match new designs + [\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699) + * Add clipboard write permission for widgets + [\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725) + * Fix widget resizing + [\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722) + * Option for audio streaming + [\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707) + * Show a specific error for hs_disabled + [\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576) + * Add Edge to the targets list + [\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721) + * File drop UI fixes and improvements + [\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505) + * Fix Bottom border of state counters is white on the dark theme + [\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715) + * Trim spurious whitespace of nicknames + [\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332) + * Ensure HostSignupDialog border colour matches light theme + [\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716) + * Don't place another call if there's already one ongoing + [\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712) + * Space room hierarchies + [\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706) + * Iterate Space view and right panel + [\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705) + * Add a scroll to bottom on message sent setting + [\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692) + * Add .tmp files to gitignore + [\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708) + * Initial Space Room View and Creation UX + [\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704) + * Add multi language spell check + [\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452) + * Fix tetris effect (holes) in read receipts + [\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697) + * Fixed edit for markdown images + [\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703) + * Iterate Space Panel + [\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702) + * Fix read receipts for compact layout + [\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700) + * Space Store and Space Panel for Room List filtering + [\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689) + * Log when turn creds expire + [\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691) + * Null check for maxHeight in call view + [\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690) + * Autocomplete invited users + [\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687) + * Add send message button + [\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535) + * Move call buttons to the room header + [\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693) + * Use the default SSSS key if the default is set + [\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638) + * Initial Spaces feature flag + [\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668) + * Clean up code edge cases and add helpers + [\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667) + * Clean up widgets when leaving the room + [\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684) + * Fix read receipts? + [\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567) + * Fix MAU usage alerts + [\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678) + Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) From ad1f9edba8297495bf54675bf7c3555fa0b197b6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:31:19 +0000 Subject: [PATCH 10/49] v3.16.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e7fff438ff..080b16b4ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.15.0", + "version": "3.16.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -27,7 +27,7 @@ "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -189,5 +189,6 @@ "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" ] - } + }, + "typings": "./lib/index.d.ts" } From 79ba898b3d6fc1264e785d24fbdfb3a8a9020854 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 18:08:55 +0000 Subject: [PATCH 11/49] Prepare changelog for v3.16.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ffeb5fbf..0b130d1c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2) + + * Fixed incorrect build output in rc.1 + Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) From 71a3847c35aac3ad58bf49b5e15d986b55d32464 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 18:08:56 +0000 Subject: [PATCH 12/49] v3.16.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 080b16b4ea..ddc352104c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.16.0-rc.1", + "version": "3.16.0-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 93f7f13c442c0faefd15d04e512bc3575c784585 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 18:16:27 -0700 Subject: [PATCH 13/49] Early proof of concept for media customization support --- src/customisations/Media.ts | 138 ++++++++++++++++++ .../models/IMediaEventContent.ts | 87 +++++++++++ src/utils/DecryptFile.js | 5 +- 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/customisations/Media.ts create mode 100644 src/customisations/models/IMediaEventContent.ts diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts new file mode 100644 index 0000000000..27abc6bc50 --- /dev/null +++ b/src/customisations/Media.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {MatrixClientPeg} from "../MatrixClientPeg"; +import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; + +// Populate this class with the details of your customisations when copying it. + +// Implementation note: The Media class must complete the contract as shown here, though +// the constructor can be whatever is relevant to your implementation. The mediaForX +// functions below create an instance of the Media class and are used throughout the +// project. + +/** + * A media object is a representation of a "source media" and an optional + * "thumbnail media", derived from event contents or external sources. + */ +export class Media { + // Per above, this constructor signature can be whatever is helpful for you. + constructor(private prepared: IPreparedMedia) { + } + + /** + * The MXC URI of the source media. + */ + public get srcMxc(): string { + return this.prepared.mxc; + } + + /** + * The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined + * otherwise. + */ + public get thumbnailMxc(): string | undefined | null { + return this.prepared.thumbnail?.mxc; + } + + /** + * Whether or not a thumbnail is recorded for this media. + */ + public get hasThumbnail(): boolean { + return !!this.thumbnailMxc; + } + + /** + * The HTTP URL for the source media. + */ + public get srcHttp(): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); + } + + /** + * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail + * is recorded for this media. Returns null/undefined otherwise. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); + } + + /** + * Gets the HTTP URL for a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); + } + + /** + * Downloads the source media. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadSource(): Promise { + return fetch(this.srcHttp); + } + + /** + * Downloads the thumbnail media with the requested characteristics. If no thumbnail media is present, + * this throws an exception. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail"); + return fetch(this.getThumbnailHttp(width, height, mode)); + } + + /** + * Downloads a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + return fetch(this.getThumbnailOfSourceHttp(width, height, mode)); + } +} + +/** + * Creates a media object from event content. + * @param {IMediaEventContent} content The event content. + * @returns {Media} The media object. + */ +export function mediaFromContent(content: IMediaEventContent): Media { + return new Media(prepEventContentAsMedia(content)); +} + +/** + * Creates a media object from an MXC URI. + * @param {string} mxc The MXC URI. + * @returns {Media} The media object. + */ +export function mediaFromMxc(mxc: string): Media { + return mediaFromContent({url: mxc}); +} diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts new file mode 100644 index 0000000000..0211a63787 --- /dev/null +++ b/src/customisations/models/IMediaEventContent.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: These types should be elsewhere. + +export interface IEncryptedFile { + url: string; + key: { + alg: string; + key_ops: string[]; + kty: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: {[alg: string]: string}; + v: string; +} + +export interface IMediaEventContent { + url?: string; // required on unencrypted media + file?: IEncryptedFile; // required for *encrypted* media + info?: { + thumbnail_url?: string; + thumbnail_file?: IEncryptedFile; + }; +} + +export interface IPreparedMedia extends IMediaObject { + thumbnail?: IMediaObject; +} + +export interface IMediaObject { + mxc: string; + file?: IEncryptedFile; +} + +/** + * Parses an event content body into a prepared media object. This prepared media object + * can be used with other functions to manipulate the media. + * @param {IMediaEventContent} content Unredacted media event content. See interface. + * @returns {IPreparedMedia} A prepared media object. + * @throws Throws if the given content cannot be packaged into a prepared media object. + */ +export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia { + let thumbnail: IMediaObject = null; + if (content?.info?.thumbnail_url) { + thumbnail = { + mxc: content.info.thumbnail_url, + file: content.info.thumbnail_file, + }; + } else if (content?.info?.thumbnail_file?.url) { + thumbnail = { + mxc: content.info.thumbnail_file.url, + file: content.info.thumbnail_file, + }; + } + + if (content?.url) { + return { + thumbnail, + mxc: content.url, + file: content.file, + }; + } else if (content?.file?.url) { + return { + thumbnail, + mxc: content.file.url, + file: content.file, + }; + } + + throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?"); +} diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index d3625d614a..fb3600cd79 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -19,6 +19,7 @@ limitations under the License. import encrypt from 'browser-encrypt-attachment'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import {MatrixClientPeg} from '../MatrixClientPeg'; +import {mediaFromContent} from "../customisations/Media"; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() @@ -87,9 +88,9 @@ const ALLOWED_BLOB_MIMETYPES = { * @returns {Promise} */ export function decryptFile(file) { - const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return Promise.resolve(fetch(url)).then(function(response) { + return media.downloadSource().then(function(response) { return response.arrayBuffer(); }).then(function(responseData) { // Decrypt the array buffer using the information taken from From 53935782bcaa51daab0011ff16236943e994f153 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 18:22:57 -0700 Subject: [PATCH 14/49] Convert DecryptFile to TS and modernize a bit --- .../models/IMediaEventContent.ts | 1 + src/utils/{DecryptFile.js => DecryptFile.ts} | 63 +++++++++---------- 2 files changed, 30 insertions(+), 34 deletions(-) rename src/utils/{DecryptFile.js => DecryptFile.ts} (76%) diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index 0211a63787..4cbe07dbd5 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -18,6 +18,7 @@ export interface IEncryptedFile { url: string; + mimetype?: string; key: { alg: string; key_ops: string[]; diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.ts similarity index 76% rename from src/utils/DecryptFile.js rename to src/utils/DecryptFile.ts index fb3600cd79..93cedbc707 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2016, 2018, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +16,8 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from 'browser-encrypt-attachment'; -// Grab the client so that we can turn mxc:// URLs into https:// URLS. -import {MatrixClientPeg} from '../MatrixClientPeg'; import {mediaFromContent} from "../customisations/Media"; +import {IEncryptedFile} from "../customisations/models/IMediaEventContent"; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() @@ -55,48 +53,46 @@ import {mediaFromContent} from "../customisations/Media"; // For the record, mime-types which must NEVER enter this list below include: // text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. -const ALLOWED_BLOB_MIMETYPES = { - 'image/jpeg': true, - 'image/gif': true, - 'image/png': true, +const ALLOWED_BLOB_MIMETYPES = [ + 'image/jpeg', + 'image/gif', + 'image/png', - 'video/mp4': true, - 'video/webm': true, - 'video/ogg': true, + 'video/mp4', + 'video/webm', + 'video/ogg', - 'audio/mp4': true, - 'audio/webm': true, - 'audio/aac': true, - 'audio/mpeg': true, - 'audio/ogg': true, - 'audio/wave': true, - 'audio/wav': true, - 'audio/x-wav': true, - 'audio/x-pn-wav': true, - 'audio/flac': true, - 'audio/x-flac': true, -}; + 'audio/mp4', + 'audio/webm', + 'audio/aac', + 'audio/mpeg', + 'audio/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wav', + 'audio/flac', + 'audio/x-flac', +]; /** * Decrypt a file attached to a matrix event. - * @param {Object} file The json taken from the matrix event. + * @param {IEncryptedFile} file The json taken from the matrix event. * This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments} * as the encryption info object, so will also have the those keys in addition to * the keys below. - * @param {string} file.url An mxc:// URL for the encrypted file. - * @param {string} file.mimetype The MIME-type of the plaintext file. - * @returns {Promise} + * @returns {Promise} Resolves to a Blob of the file. */ -export function decryptFile(file) { +export function decryptFile(file: IEncryptedFile): Promise { const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return media.downloadSource().then(function(response) { + return media.downloadSource().then((response) => { return response.arrayBuffer(); - }).then(function(responseData) { + }).then((responseData) => { // Decrypt the array buffer using the information taken from // the event content. return encrypt.decryptAttachment(responseData, file); - }).then(function(dataArray) { + }).then((dataArray) => { // Turn the array into a Blob and give it the correct MIME-type. // IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise @@ -104,11 +100,10 @@ export function decryptFile(file) { // browser (e.g. by copying the URI into a new tab or window.) // See warning at top of file. let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : ''; - if (!ALLOWED_BLOB_MIMETYPES[mimetype]) { + if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { mimetype = 'application/octet-stream'; } - const blob = new Blob([dataArray], {type: mimetype}); - return blob; + return new Blob([dataArray], {type: mimetype}); }); } From 1ac12479ca7b132b9d9d4eddc64ed6aa9b7fbde0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 19:06:46 -0700 Subject: [PATCH 15/49] Convert cases of mxcUrlToHttp to new media customisation --- src/HtmlUtils.tsx | 9 ++--- src/Notifier.ts | 3 +- src/autocomplete/CommunityProvider.tsx | 3 +- src/components/structures/GroupView.js | 11 +++--- src/components/structures/LeftPanel.tsx | 3 +- .../structures/SpaceRoomDirectory.tsx | 15 ++------ src/components/views/avatars/GroupAvatar.tsx | 9 +++-- .../views/dialogs/ConfirmUserActionDialog.js | 6 ++- .../dialogs/EditCommunityPrototypeDialog.tsx | 3 +- .../views/dialogs/IncomingSasDialog.js | 20 +++++----- src/components/views/elements/AddressTile.js | 5 +-- src/components/views/elements/Flair.js | 4 +- src/components/views/elements/Pill.js | 3 +- src/components/views/elements/SSOButtons.tsx | 3 +- src/components/views/elements/TagTile.js | 13 ++++--- .../views/groups/GroupInviteTile.js | 6 ++- .../views/groups/GroupMemberTile.js | 8 ++-- src/components/views/groups/GroupRoomInfo.js | 7 ++-- src/components/views/groups/GroupRoomTile.js | 8 ++-- src/components/views/groups/GroupTile.js | 6 ++- src/components/views/messages/MAudioBody.js | 7 ++-- src/components/views/messages/MFileBody.js | 5 ++- src/components/views/messages/MImageBody.js | 36 +++++++----------- src/components/views/messages/MVideoBody.tsx | 14 ++++--- .../views/messages/RoomAvatarEvent.js | 3 +- src/components/views/right_panel/UserInfo.tsx | 3 +- .../room_settings/RoomProfileSettings.js | 5 ++- .../views/rooms/LinkPreviewWidget.js | 9 +++-- src/components/views/settings/ChangeAvatar.js | 3 +- .../views/settings/ProfileSettings.js | 5 ++- src/customisations/Media.ts | 38 +++++++++++++++++-- src/customisations/models/ResizeMode.ts | 17 +++++++++ src/stores/OwnProfileStore.ts | 9 ++++- 33 files changed, 178 insertions(+), 121 deletions(-) create mode 100644 src/customisations/models/ResizeMode.ts diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7d6b049914..12752eb20f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -36,6 +36,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; +import {mediaFromMxc} from "./customisations/Media"; linkifyMatrix(linkify); @@ -181,11 +182,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { return { tagName, attribs: {}}; } - attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - attribs.src, - attribs.width || 800, - attribs.height || 600, - ); + const width = Number(attribs.width) || 800; + const height = Number(attribs.height) || 600; + attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); return { tagName, attribs }; }, 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { diff --git a/src/Notifier.ts b/src/Notifier.ts index 6460be20ad..f68bfabc18 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel"; import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers"; import RoomViewStore from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; +import {mediaFromMxc} from "./customisations/Media"; /* * Dispatches: @@ -150,7 +151,7 @@ export const Notifier = { // Ideally in here we could use MSC1310 to detect the type of file, and reject it. return { - url: MatrixClientPeg.get().mxcUrlToHttp(content.url), + url: mediaFromMxc(content.url).srcHttp, name: content.name, type: content.type, size: content.size, diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..b7a4e0960e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -27,6 +27,7 @@ import {sortBy} from "lodash"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; +import {mediaFromMxc} from "../customisations/Media"; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider { name={name || groupId} width={24} height={24} - url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} /> + url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} /> ), range, diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b4b871a0b4..f05d8d0758 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( @@ -368,8 +369,7 @@ class FeaturedUser extends React.Component { const permalink = makeUserPermalink(this.props.summaryInfo.user_id); const userNameNode = { name }; - const httpUrl = MatrixClientPeg.get() - .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); + const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64); const deleteButton = this.props.editing ?
; } - const httpInviterAvatar = this.state.inviterProfile ? - this._matrixClient.mxcUrlToHttp( - this.state.inviterProfile.avatarUrl, 36, 36, - ) : null; + const httpInviterAvatar = this.state.inviterProfile + ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) + : null; const inviter = group.inviter || {}; let inviterName = inviter.userId; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 88c7a71b35..f7865d094a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -41,6 +41,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import SpacePanel from "../views/spaces/SpacePanel"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { isMinimized: boolean; @@ -121,7 +122,7 @@ export default class LeftPanel extends React.Component { let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); if (settingBgMxc) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); + avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); } const avatarUrlProp = `url(${avatarUrl})`; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 72e52678b6..9ee16558d3 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { space: Room; @@ -158,12 +159,7 @@ const SubSpace: React.FC = ({ let url: string; if (space.avatar_url) { - url = MatrixClientPeg.get().mxcUrlToHttp( - space.avatar_url, - Math.floor(24 * window.devicePixelRatio), - Math.floor(24 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio)); } return
@@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli let url: string; if (room.avatar_url) { - url = cli.mxcUrlToHttp( - room.avatar_url, - Math.floor(32 * window.devicePixelRatio), - Math.floor(32 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio)); } const content = diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index a033257871..dc363da304 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import React from 'react'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import BaseAvatar from './BaseAvatar'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; +import {ResizeMode} from "../../../customisations/models/ResizeMode"; export interface IProps { groupId?: string; @@ -25,7 +27,7 @@ export interface IProps { groupAvatarUrl?: string; width?: number; height?: number; - resizeMethod?: string; + resizeMethod?: ResizeMode; onClick?: React.MouseEventHandler; } @@ -38,8 +40,7 @@ export default class GroupAvatar extends React.Component { }; getGroupAvatarUrl() { - return MatrixClientPeg.get().mxcUrlToHttp( - this.props.groupAvatarUrl, + return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp( this.props.width, this.props.height, this.props.resizeMethod, diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 8827f161f1..8cfd28986b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; /* * A dialog for confirming an operation on another user. @@ -108,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component { name = this.props.member.name; userId = this.props.member.userId; } else { - const httpAvatarUrl = this.props.groupMember.avatarUrl ? - this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null; + const httpAvatarUrl = this.props.groupMember.avatarUrl + ? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48) + : null; name = this.props.groupMember.displayname || this.props.groupMember.userId; userId = this.props.groupMember.userId; avatar = ; diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 504d563bd9..ee3696b427 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import FlairStore from "../../../stores/FlairStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { communityId: string; @@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent; if (!this.state.avatarPreview) { if (this.state.currentAvatarUrl) { - const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl); + const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp; preview = ; } else { preview =
diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index d65ec7563f..f18b7a9d0c 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; const PHASE_START = 0; const PHASE_SHOW_SAS = 1; @@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component { const Spinner = sdk.getComponent("views.elements.Spinner"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId(); + const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId(); let profile; - if (this.state.opponentProfile) { + const oppProfile = this.state.opponentProfile; + if (oppProfile) { + const url = oppProfile.avatar_url + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + : null; profile =
- -

{this.state.opponentProfile.displayname}

+

{oppProfile.displayname}

; } else if (this.state.opponentProfileError) { profile =
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 4a216dbae4..4f5ee45a3c 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.elements.AddressTile") export default class AddressTile extends React.Component { @@ -47,9 +48,7 @@ export default class AddressTile extends React.Component { const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType); if (isMatrixAddress && address.avatarMxc) { - imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp( - address.avatarMxc, 25, 25, 'crop', - )); + imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25)); } else if (address.addressType === 'email') { imgUrls.push(require("../../../../res/img/icon-email-user.svg")); } diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 75998cb721..73d5b91511 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher/dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; class FlairAvatar extends React.Component { @@ -39,8 +40,7 @@ class FlairAvatar extends React.Component { } render() { - const httpUrl = this.context.mxcUrlToHttp( - this.props.groupProfile.avatarUrl, 16, 16, 'scale', false); + const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16); const tooltip = this.props.groupProfile.name ? `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`: this.props.groupProfile.groupId; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index b0d4fc7fa2..bf99ee6078 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -26,6 +26,7 @@ import FlairStore from "../../../stores/FlairStore"; import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import {mediaFromMxc} from "../../../customisations/Media"; import Tooltip from './Tooltip'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -259,7 +260,7 @@ class Pill extends React.Component { linkText = groupId; if (this.props.shouldShowPillAvatar) { avatar =
); + const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800); + avatarElement =
; } const groupRoomName = this.state.groupRoom.displayname; diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 8b25437f71..7edfc1a376 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher'; import { GroupRoomType } from '../../../groups'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.groups.GroupRoomTile") class GroupRoomTile extends React.Component { @@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component { render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const avatarUrl = this.context.mxcUrlToHttp( - this.props.groupRoom.avatarUrl, - 36, 36, 'crop', - ); + const avatarUrl = this.props.groupRoom.avatarUrl + ? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36) + : null; const av = ( { profile.shortDescription }
:
; - const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; + const httpUrl = profile.avatarUrl + ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight) + : null; let avatarElement = (
diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 498e2db12a..78ded9a514 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MAudioBody") export default class MAudioBody extends React.Component { @@ -41,11 +42,11 @@ export default class MAudioBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index e9893f99b6..07d7beb793 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -27,6 +27,7 @@ import request from 'browser-request'; import Modal from '../../../Modal'; import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; // A cached tinted copy of require("../../../../res/img/download.svg") @@ -178,8 +179,8 @@ export default class MFileBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + const media = mediaFromContent(this.props.mxEvent.getContent()); + return media.srcHttp; } componentDidMount() { diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 59c5b4e66b..0a1f875935 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MImageBody") export default class MImageBody extends React.Component { @@ -167,16 +168,16 @@ export default class MImageBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return this.context.mxcUrlToHttp(content.url); + return media.srcHttp; } } _getThumbUrl() { - // FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600. + // FIXME: we let images grow as wide as you like, rather than capped to 800x600. // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // thumbnail resolution will be unnecessarily reduced. // custom timeline widths seems preferable. @@ -185,21 +186,19 @@ export default class MImageBody extends React.Component { const thumbHeight = Math.round(600 * pixelRatio); const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + + if (media.isEncrypted) { // Don't use the thumbnail for clients wishing to autoplay gifs. if (this.state.decryptedThumbnailUrl) { return this.state.decryptedThumbnailUrl; } return this.state.decryptedUrl; - } else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) { + } else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) { // special case to return clientside sender-generated thumbnails for SVGs, if any, // given we deliberately don't thumbnail them serverside to prevent // billion lol attacks and similar - return this.context.mxcUrlToHttp( - content.info.thumbnail_url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale'); } else { // we try to download the correct resolution // for hi-res images (like retina screenshots). @@ -218,7 +217,7 @@ export default class MImageBody extends React.Component { pixelRatio === 1.0 || (!info || !info.w || !info.h || !info.size) ) { - return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // we should only request thumbnails if the image is bigger than 800x600 // (or 1600x1200 on retina) otherwise the image in the timeline will just @@ -233,24 +232,17 @@ export default class MImageBody extends React.Component { info.w > thumbWidth || info.h > thumbHeight ); - const isLargeFileSize = info.size > 1*1024*1024; + const isLargeFileSize = info.size > 1*1024*1024; // 1mb if (isLargeFileSize && isLargerThanThumbnail) { // image is too large physically and bytewise to clutter our timeline so // we ask for a thumbnail, despite knowing that it will be max 800x600 // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). - return this.context.mxcUrlToHttp( - content.url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // download the original image otherwise, so we can scale it client side // to take pixelRatio into account. - // ( no width/height means we want the original image) - return this.context.mxcUrlToHttp( - content.url, - ); + return media.srcHttp; } } } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 89985dee7d..32b071ea24 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; interface IProps { /* the MatrixEvent to show */ @@ -76,11 +77,11 @@ export default class MVideoBody extends React.PureComponent { } private getContentUrl(): string|null { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } @@ -91,10 +92,11 @@ export default class MVideoBody extends React.PureComponent { private getThumbUrl(): string|null { const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + if (media.isEncrypted) { return this.state.decryptedThumbnailUrl; - } else if (content.info && content.info.thumbnail_url) { - return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url); + } else if (media.hasThumbnail) { + return media.thumbnailHttp; } else { return null; } diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index ba860216f0..00aaf9bfda 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -24,6 +24,7 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.messages.RoomAvatarEvent") export default class RoomAvatarEvent extends React.Component { @@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component { onAvatarClick = () => { const cli = MatrixClientPeg.get(); const ev = this.props.mxEvent; - const httpUrl = cli.mxcUrlToHttp(ev.getContent().url); + const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; const room = cli.getRoom(this.props.mxEvent.getRoomId()); const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index eb47a56269..d415d19852 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,6 +63,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IDevice { deviceId: string; @@ -1408,7 +1409,7 @@ const UserInfoHeader: React.FC<{ const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl; if (!avatarUrl) return; - const httpUrl = cli.mxcUrlToHttp(avatarUrl); + const httpUrl = mediaFromMxc(avatarUrl).srcHttp; const params = { src: httpUrl, name: member.name, diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js index 563368384b..3dbe2b2b7f 100644 --- a/src/components/views/room_settings/RoomProfileSettings.js +++ b/src/components/views/room_settings/RoomProfileSettings.js @@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; // TODO: Merge with ProfileSettings? @replaceableComponent("views.room_settings.RoomProfileSettings") @@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component { const avatarEvent = room.currentState.getStateEvents("m.room.avatar", ""); let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); const topicEvent = room.currentState.getStateEvents("m.room.topic", ""); const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : ''; @@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component { if (this.state.avatarFile) { const uri = await client.uploadContent(this.state.avatarFile); await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, ''); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); newState.originalAvatarUrl = newState.avatarUrl; newState.avatarFile = null; } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 39c9f0bcf7..536abf57fc 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -26,6 +26,7 @@ import Modal from "../../../Modal"; import * as ImageUtils from "../../../ImageUtils"; import { _t } from "../../../languageHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.rooms.LinkPreviewWidget") export default class LinkPreviewWidget extends React.Component { @@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component { let src = p["og:image"]; if (src && src.startsWith("mxc://")) { - src = MatrixClientPeg.get().mxcUrlToHttp(src); + src = mediaFromMxc(src).srcHttp; } const params = { @@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component { if (!SettingsStore.getValue("showImages")) { image = null; // Don't render a button to show the image, just hide it outright } - const imageMaxWidth = 100; const imageMaxHeight = 100; + const imageMaxWidth = 100; + const imageMaxHeight = 100; if (image && image.startsWith("mxc://")) { - image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); + // We deliberately don't want a square here, so use the source HTTP thumbnail function + image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale'); } let thumbHeight = imageMaxHeight; diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 8067046ffd..0b6739df64 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.settings.ChangeAvatar") export default class ChangeAvatar extends React.Component { @@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component { httpPromise.then(function() { self.setState({ phase: ChangeAvatar.Phases.Display, - avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), + avatarUrl: mediaFromMxc(newUrl).srcHttp, }); }, function(error) { self.setState({ diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 30dcdc3c47..971b868751 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.settings.ProfileSettings") export default class ProfileSettings extends React.Component { @@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component { const client = MatrixClientPeg.get(); let avatarUrl = OwnProfileStore.instance.avatarMxc; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); this.state = { userId: client.getUserId(), originalDisplayName: OwnProfileStore.instance.displayName, @@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component { ` (${this.state.avatarFile.size}) bytes`); const uri = await client.uploadContent(this.state.avatarFile); await client.setAvatarUrl(uri); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); newState.originalAvatarUrl = newState.avatarUrl; newState.avatarFile = null; } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 27abc6bc50..f42307c530 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -16,6 +16,7 @@ import {MatrixClientPeg} from "../MatrixClientPeg"; import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; +import {ResizeMode} from "./models/ResizeMode"; // Populate this class with the details of your customisations when copying it. @@ -33,6 +34,13 @@ export class Media { constructor(private prepared: IPreparedMedia) { } + /** + * True if the media appears to be encrypted. Actual file contents may vary. + */ + public get isEncrypted(): boolean { + return !!this.prepared.file; + } + /** * The MXC URI of the source media. */ @@ -62,6 +70,15 @@ export class Media { return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); } + /** + * The HTTP URL for the thumbnail media (without any specified width, height, etc). Null/undefined + * if no thumbnail media recorded. + */ + public get thumbnailHttp(): string | undefined | null { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc); + } + /** * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail * is recorded for this media. Returns null/undefined otherwise. @@ -70,7 +87,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined { + public getThumbnailHttp(width: number, height: number, mode: ResizeMode = "scale"): string | null | undefined { if (!this.hasThumbnail) return null; return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); } @@ -82,10 +99,23 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string { + public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMode = "scale"): string { return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); } + /** + * Creates a square thumbnail of the media. If the media has a thumbnail recorded, that MXC will + * be used, otherwise the source media will be used. + * @param {number} dim The desired width and height. + * @returns {string} An HTTP URL for the thumbnail. + */ + public getSquareThumbnailHttp(dim: number): string { + if (this.hasThumbnail) { + return this.getThumbnailHttp(dim, dim, 'crop'); + } + return this.getThumbnailOfSourceHttp(dim, dim, 'crop'); + } + /** * Downloads the source media. * @returns {Promise} Resolves to the server's response for chaining. @@ -102,7 +132,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {Promise} Resolves to the server's response for chaining. */ - public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise { if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail"); return fetch(this.getThumbnailHttp(width, height, mode)); } @@ -114,7 +144,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {Promise} Resolves to the server's response for chaining. */ - public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise { return fetch(this.getThumbnailOfSourceHttp(width, height, mode)); } } diff --git a/src/customisations/models/ResizeMode.ts b/src/customisations/models/ResizeMode.ts new file mode 100644 index 0000000000..401b6723e5 --- /dev/null +++ b/src/customisations/models/ResizeMode.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type ResizeMode = "scale" | "crop"; diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index 8983380fec..5e722877e2 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -22,6 +22,7 @@ import { User } from "matrix-js-sdk/src/models/user"; import { throttle } from "lodash"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; +import {mediaFromMxc} from "../customisations/Media"; interface IState { displayName?: string; @@ -72,8 +73,12 @@ export class OwnProfileStore extends AsyncStoreWithClient { */ public getHttpAvatarUrl(size = 0): string { if (!this.avatarMxc) return null; - const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through - return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize); + const media = mediaFromMxc(this.avatarMxc); + if (!size || size <= 0) { + return media.srcHttp; + } else { + return media.getSquareThumbnailHttp(size); + } } protected async onNotReady() { From fa5d98c319c5cddea8fe94c0cc70215e7909401f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Mar 2021 18:45:09 -0700 Subject: [PATCH 16/49] Convert cases of getHttpUriForMxc to new media customisation --- src/Avatar.ts | 10 +++------- src/components/structures/RoomDirectory.js | 9 ++++----- src/components/views/avatars/RoomAvatar.tsx | 16 ++++++++-------- src/components/views/avatars/WidgetAvatar.tsx | 4 ++-- .../dialogs/CommunityPrototypeInviteDialog.tsx | 10 ++++++---- src/components/views/dialogs/InviteDialog.tsx | 14 +++++++------- src/components/views/rooms/RoomDetailRow.js | 10 +++++----- src/components/views/settings/BridgeTile.tsx | 8 ++------ 8 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index e2557e21a8..eeef3e2c69 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import {User} from "matrix-js-sdk/src/models/user"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; +import {mediaFromMxc} from "./customisations/Media"; export type ResizeMethod = "crop" | "scale"; @@ -47,16 +47,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu } export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { - const url = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, + if (!user.avatarUrl) return null; + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), resizeMethod, ); - if (!url || url.length === 0) { - return null; - } - return url; } function isValidHexColor(color: string): boolean { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 363c67262b..3613261da6 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; @@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore"; import FlairStore from "../../stores/FlairStore"; import CountlyAnalytics from "../../CountlyAnalytics"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; @@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component { topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } topic = linkifyAndSanitizeHtml(topic); - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - room.avatar_url, 32, 32, "crop", - ); + let avatarUrl = null; + if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); + return [
this.onRoomClicked(room, ev)} diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 0a59f6e36a..31245b44b7 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; -import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -24,6 +23,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, @@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component { }; private static getImageUrls(props: IProps): string[] { - return [ - getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - // Default props don't play nicely with getDerivedStateFromProps - //props.oobData !== undefined ? props.oobData.avatarUrl : {}, - props.oobData.avatarUrl, + let oobAvatar = null; + if (props.oobData.avatarUrl) { + oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, - ), // highest priority + ); + } + return [ + oobAvatar, // highest priority RoomAvatar.getRoomAvatarUrl(props), ].filter(function(url) { return (url !== null && url !== ""); diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index 04cfce7670..6468b0dd49 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -16,11 +16,11 @@ limitations under the License. import React, {ComponentProps, useContext} from 'react'; import classNames from 'classnames'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {IApp} from "../../../stores/WidgetStore"; import BaseAvatar, {BaseAvatarType} from "./BaseAvatar"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "url" | "urls"> { app: IApp; @@ -47,7 +47,7 @@ const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 2 name={app.id} className={classNames("mx_WidgetAvatar", className)} // MSC2765 - url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined} + url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined} urls={iconUrls} width={width} height={height} diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index d1080566ac..2635f95bb7 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import InviteDialog from "./InviteDialog"; import BaseAvatar from "../avatars/BaseAvatar"; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite"; import StyledCheckbox from "../elements/StyledCheckbox"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { roomId: string; @@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< private renderPerson(person: IPerson, key: any) { const avatarSize = 36; + let avatarUrl = null; + if (person.user.getMxcAvatarUrl()) { + avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize); + } return (
{ width={avatarSize} height={avatarSize} /> : { src={require("../../../../res/img/icon-email-pill-avatar.svg")} width={avatarSize} height={avatarSize} /> : ) :
; + let avatarUrl = null; + if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24); + return + url={avatarUrl} />
{ name }
  diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx index b33219ad4a..3565d1ba2e 100644 --- a/src/components/views/settings/BridgeTile.tsx +++ b/src/components/views/settings/BridgeTile.tsx @@ -16,9 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {_t} from "../../../languageHandler"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Pill from "../elements/Pill"; import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; import BaseAvatar from "../avatars/BaseAvatar"; @@ -27,6 +25,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { isUrlPermitted } from '../../../HtmlUtils'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps { ev: MatrixEvent; @@ -114,10 +113,7 @@ export default class BridgeTile extends React.PureComponent { let networkIcon; if (protocol.avatar_url) { - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - protocol.avatar_url, 64, 64, "crop", - ); + const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64); networkIcon = Date: Fri, 5 Mar 2021 18:49:32 -0700 Subject: [PATCH 17/49] Appease the linter --- src/HtmlUtils.tsx | 1 - src/components/structures/LeftPanel.tsx | 1 - src/components/views/avatars/GroupAvatar.tsx | 1 - src/components/views/avatars/WidgetAvatar.tsx | 5 +---- src/components/views/elements/AddressTile.js | 1 - src/components/views/elements/Pill.js | 10 ++++------ src/components/views/messages/MAudioBody.js | 1 - src/components/views/messages/MFileBody.js | 1 - src/components/views/messages/MVideoBody.tsx | 1 - src/components/views/right_panel/UserInfo.tsx | 2 +- src/customisations/models/IMediaEventContent.ts | 6 +++--- test/components/structures/GroupView-test.js | 3 ++- 12 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 12752eb20f..59b596a5da 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -32,7 +32,6 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from './settings/SettingsStore'; import cheerio from 'cheerio'; -import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f7865d094a..9a1ce63785 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -36,7 +36,6 @@ import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import SpacePanel from "../views/spaces/SpacePanel"; diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index dc363da304..321ca025a3 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import BaseAvatar from './BaseAvatar'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index 6468b0dd49..cca158269e 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ComponentProps, useContext} from 'react'; +import React, {ComponentProps} from 'react'; import classNames from 'classnames'; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {IApp} from "../../../stores/WidgetStore"; import BaseAvatar, {BaseAvatarType} from "./BaseAvatar"; import {mediaFromMxc} from "../../../customisations/Media"; @@ -27,8 +26,6 @@ interface IProps extends Omit, "name" | "url" | " } const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 20, ...props }) => { - const cli = useContext(MatrixClientContext); - let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")]; // heuristics for some better icons until Widgets support their own icons if (app.type.includes("jitsi")) { diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 4f5ee45a3c..df66d10a71 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -19,7 +19,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from "../../../index"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; import {replaceableComponent} from "../../../utils/replaceableComponent"; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index bf99ee6078..e61d312305 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2017 - 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -255,12 +253,12 @@ class Pill extends React.Component { case Pill.TYPE_GROUP_MENTION: { if (this.state.group) { const {avatarUrl, groupId, name} = this.state.group; - const cli = MatrixClientPeg.get(); linkText = groupId; if (this.props.shouldShowPillAvatar) { - avatar =