diff --git a/src/components/views/messages/BridgeError.js b/src/components/views/messages/BridgeError.js index 3bc485a70b8e..cd2713f0bc1e 100644 --- a/src/components/views/messages/BridgeError.js +++ b/src/components/views/messages/BridgeError.js @@ -14,13 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { strict as assert } from 'assert'; - import React from 'react'; import PropTypes from 'prop-types'; import { EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk'; import { _t, _td } from '../../../languageHandler'; +import { withRelation } from '../../../wrappers/withRelation.js'; /** @@ -42,200 +41,18 @@ function assureArray(arr) { } /** - * A watcher noticing changes to the relations of an event. - * - * Whenever a change to the relations of the event is registered, the callbacks - * are triggered. The callbacks are passed the new set of relation events. For - * ease of use the callbacks are also called directly after their registration - * if there are already relations. - * - * Usage: - * 1. Initialize in Component constructor - * 2. register() in componentDidMount - * 3. teardown() in componentWillUnmount - * - * Example for register(): - * - * ``` - * this.relationsWatcher.register(r => this.setState({relations: r})); - * ``` - * - * @param {string} relationType The type of relation for which to filter related events. - * @param {string} eventType The type of event for which to filter related events. - * @param {string} mxEvent The Matrix event for which changes to its relations - * should be watched. - * @param {string} room The Matrix room the event belongs to. + * In case there are bridge errors related to the given event, show them. */ -class RelationsWatcher { - constructor(relationType, eventType, mxEvent, room) { - this.relationType = relationType; - this.eventType = eventType; - this.room = room; - this.mxEvent = mxEvent; - - this.listenersAdded = false; - this.creationListenerTarget = null; - this.relations = null; - this.callbacks = []; - - this._setup(mxEvent, room); - } - - /** - * Cleanup method. Call when the watcher is no longer needed e.g. in a - * Components `componentWillUnmount` method. - */ - teardown() { - if (!this.relations) { - return; - } - this._removeListeners(this.relations); - this._removeCreationListener(); - // this.callbacks = []; - this.relations = null; - - assert(!this.listenersAdded); - assert(!this.creationListenerTarget); - } - - /** - * Register a callback for the watcher. - - * The callback is called when the relations of the event change and when - * there are relations to an event at the time of registering. - * - * Use it e.g. in a Components `componentDidMount` method. - * - * @param {RelationsWatcher~onChangeCallback} onChangeCallback - */ - register(onChangeCallback) { - this.callbacks.push(onChangeCallback); - if (this.relations) { - const ret = this._getRelationsEvents(); - onChangeCallback(ret); - } - } - - _setup(mxEvent, room) { - assert(!this.relations); - assert(!this.listenersAdded); - - this.relations = this._getRelations(mxEvent, room); - - if (!this.relations) { - // No setup happened. Wait for relations to appear. - this._addCreationListener(mxEvent); - return; - } - this._removeCreationListener(); - - this._addListeners(this.relations); - - assert(this.listenersAdded); - assert(!this.creationListenerTarget); - } - - _getRelations(mxEvent, room) { - const timelineSet = room.getUnfilteredTimelineSet(); - // TODO[V02460]: Correct @returns to Optional in matrix-js-sdk. - return timelineSet.getRelationsForEvent( - mxEvent.getId(), - this.relationType, - this.eventType, - ) || null; - } - - _getRelationsEvents() { - return this.relations.getRelations() || []; - } - - // Relations creation - - _creationCallback = (relationType, eventType) => { - if (relationType != this.relationType || eventType != this.eventType) { - return; - } - this._removeCreationListener(); - this._setup(this.mxEvent, this.room); - } - - _addCreationListener(mxEvent) { - mxEvent.on("Event.relationsCreated", this._creationCallback); - this.creationListenerTarget = mxEvent; - } - - _removeCreationListener() { - if (!this.creationListenerTarget) { - return; - } - this.creationListenerTarget.removeListener( - "Event.relationsCreated", - this._creationCallback, - ); - this.creationListenerTarget = null; - } - - // Relations changes - - _notify = () => { - const ret = this._getRelationsEvents(); - this.callbacks.forEach(callback => callback(ret)); - } - - _addListeners(relations) { - if (this.listenersAdded) { - return; - } - relations.on("Relations.add", this._notify); - relations.on("Relations.remove", this._notify); - relations.on("Relations.redaction", this._notify); - this.listenersAdded = true; - } - - _removeListeners(relations) { - if (!this.listenersAdded) { - return; - } - relations.removeListener("Relations.add", this._notify); - relations.removeListener("Relations.remove", this._notify); - relations.removeListener("Relations.redaction", this._notify); - this.listenersAdded = false; - } -} - -/** - * Callback for when the set of relations belonging to an event changes. - * @callback RelationsWatcher~onChangeCallback - * @param {MatrixEvent[]} relations The set of relations. - */ - - -export default class BridgeError extends React.PureComponent { +class BridgeError extends React.PureComponent { + // export? BridgeError is not the class getting exported! See end of file. static propTypes = { mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, room: PropTypes.instanceOf(Room).isRequired, + relations: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired, }; constructor(props) { super(props); - - /** @type {errorEvents: MatrixEvent[]} */ - this.state = {errorEvents: []}; - - this.relationsWatcher = new RelationsWatcher( - "m.reference", - "de.nasnotfound.bridge_error", - props.mxEvent, - props.room, - ); - } - - componentDidMount() { - this.relationsWatcher.register(e => this.setState({errorEvents: e})); - } - - componentWillUnmount() { - this.relationsWatcher.teardown(); } /** @@ -347,7 +164,7 @@ export default class BridgeError extends React.PureComponent { } render() { - const { errorEvents } = this.state; + const errorEvents = this.props.relations; const isBridgeError = !!errorEvents.length; if (!isBridgeError) { @@ -365,3 +182,12 @@ export default class BridgeError extends React.PureComponent { ); } } + +const BridgeErrorWithRelation = withRelation( + BridgeError, + "m.reference", + "de.nasnotfound.bridge_error", +); + +export default BridgeErrorWithRelation; +// export { BridgeErrorWithRelation as BridgeError }; diff --git a/src/wrappers/withRelation.js b/src/wrappers/withRelation.js new file mode 100644 index 000000000000..3599e7f19954 --- /dev/null +++ b/src/wrappers/withRelation.js @@ -0,0 +1,174 @@ + +import { strict as assert } from 'assert'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import { EventTimeline, MatrixEvent, Room } from 'matrix-js-sdk'; + +/** + * Wraps a componenets to provide it the `relations` prop. + * + * This wrapper only provides one type of relation to its child component. + * To deliver the right type of relation to its child this function requires + * the `relationType` and `eventType` arguments to be passed and the wrapping + * compnent requires the `mxEvent` and `room` props to be set. These two props + * are passed down besides the `relations` prop. + * + * Props: + * - `mxEvent`: The event for whicht to get the relations. + * - `room`: The room in which `mxEvent` was emitted. + * + * This component requires its key attribute to be set (e.g. to + * mxEvent.getId()). This is due the fact that it does not have an update logic + * for changing props implemented. For more details see + * https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key + * + * @param {typeof React.Component} WrappedComponent The component to wrap and + * provide the relations to. + * @param {string} relationType The type of relation to filter for. + * @param {string} eventType The type of event to filter for. + * @returns {typeof React.Component} + */ +export function withRelation(WrappedComponent, relationType, eventType) { +class WithRelation extends React.PureComponent { + static propTypes = { + mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, + room: PropTypes.instanceOf(Room).isRequired, + }; + + constructor(props) { + super(props); + + this.listenersAdded = false; + this.creationListenerTarget = null; + this.relations = null; + + this.onChangeCallback = (e) => {}; + + this.state = {relations: []}; + + this._setup(); + } + + componentDidMount() { + this.onChangeCallback = (e) => this.setState({relations: e}); + + if (this.relations) { + this.onChangeCallback(this._getEvents()); + } + } + + componentWillUnmount() { + this._removeCreationListener(); + + if (this.relations) { + this._removeListeners(this.relations); + this.relations = null; + } + + assert(!this.relations); + assert(!this.listenersAdded); + assert(!this.creationListenerTarget); + } + + _setup() { + const { mxEvent, room } = this.props; + + assert(!this.relations); + assert(!this.listenersAdded); + + this.relations = this._getRelations(mxEvent, room); + + if (!this.relations) { + // No setup happened. Wait for relations to appear. + this._addCreationListener(mxEvent); + return; + } + this._removeCreationListener(); + + this._addListeners(this.relations); + + assert(this.relations); + assert(this.listenersAdded); + assert(!this.creationListenerTarget); + } + + _getRelations(mxEvent, room) { + const timelineSet = room.getUnfilteredTimelineSet(); + return timelineSet.getRelationsForEvent( + mxEvent.getId(), + relationType, + eventType, + ) || null; + } + + _getEvents() { + return this.relations.getRelations() || []; + } + + // Relations creation + + _creationCallback = (relationTypeArg, eventTypeArg) => { + if (relationTypeArg != relationType || eventTypeArg != eventType) { + return; + } + this._removeCreationListener(); + this._setup(); + } + + _addCreationListener(mxEvent) { + mxEvent.on("Event.relationsCreated", this._creationCallback); + this.creationListenerTarget = mxEvent; + } + + _removeCreationListener() { + if (!this.creationListenerTarget) { + return; + } + this.creationListenerTarget.removeListener( + "Event.relationsCreated", + this._creationCallback, + ); + this.creationListenerTarget = null; + } + + // Relations changes + + _notify = () => { + this.onChangeCallback(this._getEvents()); + } + + _addListeners(relations) { + if (this.listenersAdded) { + return; + } + relations.on("Relations.add", this._notify); + relations.on("Relations.remove", this._notify); + relations.on("Relations.redaction", this._notify); + this.listenersAdded = true; + } + + _removeListeners(relations) { + if (!this.listenersAdded) { + return; + } + relations.removeListener("Relations.add", this._notify); + relations.removeListener("Relations.remove", this._notify); + relations.removeListener("Relations.redaction", this._notify); + this.listenersAdded = false; + } + + render() { + return ( + + ); + } +} + +WithRelation.displayName = `WithRelation(${getDisplayName(WrappedComponent)})`; +return WithRelation; +} + +function getDisplayName(WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +}