Factor out generic EventListSummary from MELS

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2019-09-12 10:54:52 +01:00 committed by Bruno Windels
parent 4635b1319a
commit 34530843f4
7 changed files with 164 additions and 107 deletions

View file

@ -88,12 +88,12 @@
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_EventListSummary";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_InteractiveTooltip.scss";
@import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_MemberEventListSummary.scss";
@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_PowerSelector.scss";
@import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ProgressBar.scss";
@import "./views/elements/_ReplyThread.scss"; @import "./views/elements/_ReplyThread.scss";

View file

@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MemberEventListSummary { .mx_EventListSummary {
position: relative; position: relative;
} }
.mx_TextualEvent.mx_MemberEventListSummary_summary { .mx_TextualEvent.mx_EventListSummary_summary {
font-size: 14px; font-size: 14px;
display: inline-flex; display: inline-flex;
} }
.mx_MemberEventListSummary_avatars { .mx_EventListSummary_avatars {
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
padding-top: 8px; padding-top: 8px;
line-height: 12px; line-height: 12px;
} }
.mx_MemberEventListSummary_avatars .mx_BaseAvatar { .mx_EventListSummary_avatars .mx_BaseAvatar {
margin-right: -4px; margin-right: -4px;
cursor: pointer; cursor: pointer;
} }
.mx_MemberEventListSummary_toggle { .mx_EventListSummary_toggle {
color: $accent-color; color: $accent-color;
cursor: pointer; cursor: pointer;
float: right; float: right;
@ -43,29 +43,29 @@ limitations under the License.
margin-top: 8px; margin-top: 8px;
} }
.mx_MemberEventListSummary_line { .mx_EventListSummary_line {
border-bottom: 1px solid $primary-hairline-color; border-bottom: 1px solid $primary-hairline-color;
margin-left: 63px; margin-left: 63px;
line-height: 30px; line-height: 30px;
} }
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {
.mx_MemberEventListSummary { .mx_EventListSummary {
font-size: 13px; font-size: 13px;
.mx_EventTile_line { .mx_EventTile_line {
line-height: 20px; line-height: 20px;
} }
} }
.mx_MemberEventListSummary_line { .mx_EventListSummary_line {
line-height: 22px; line-height: 22px;
} }
.mx_MemberEventListSummary_toggle { .mx_EventListSummary_toggle {
margin-top: 3px; margin-top: 3px;
} }
.mx_TextualEvent.mx_MemberEventListSummary_summary { .mx_TextualEvent.mx_EventListSummary_summary {
font-size: 13px; font-size: 13px;
} }
} }

View file

@ -0,0 +1,95 @@
/*
Copyright 2019 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 React, {useEffect} from 'react';
import PropTypes from 'prop-types';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import {MatrixEvent, RoomMember} from "matrix-js-sdk";
import {useStateToggle} from "../../../hooks/useStateToggle";
const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
// Whenever expanded changes call onToggle
useEffect(() => {
if (onToggle) {
onToggle();
}
}, [expanded]);
const eventIds = events.map((e) => e.getId()).join(',');
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
{ children }
</div>
);
}
if (expanded) {
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
{ _t('collapse') }
</div>
<div className="mx_EventListSummary_line">&nbsp;</div>
{ children }
</div>
);
}
const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
{ _t('expand') }
</div>
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
{ avatars }
</span>
<span className="mx_TextualEvent mx_EventListSummary_summary">
{ summaryText }
</span>
</div>
</div>
</div>
);
};
EventListSummary.propTypes = {
// An array of member events to summarise
events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
// An array of EventTiles to render when expanded
children: PropTypes.arrayOf(PropTypes.element).isRequired,
// The minimum number of events needed to trigger summarisation
threshold: PropTypes.number,
// Called when the event list expansion is toggled
onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true
startExpanded: PropTypes.bool,
// The list of room members for which to show avatars next to the summary
summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
// The text to show as the summary of this event list
summaryText: PropTypes.string.isRequired,
};
export default EventListSummary;

View file

@ -19,9 +19,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import createReactClass from 'create-react-class';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import sdk from "../../../index";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'MemberEventListSummary', displayName: 'MemberEventListSummary',
@ -43,12 +43,6 @@ module.exports = createReactClass({
startExpanded: PropTypes.bool, startExpanded: PropTypes.bool,
}, },
getInitialState: function() {
return {
expanded: Boolean(this.props.startExpanded),
};
},
getDefaultProps: function() { getDefaultProps: function() {
return { return {
summaryLength: 1, summaryLength: 1,
@ -57,37 +51,27 @@ module.exports = createReactClass({
}; };
}, },
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate: function(nextProps) {
// Update if // Update if
// - The number of summarised events has changed // - The number of summarised events has changed
// - or if the summary is currently expanded
// - or if the summary is about to toggle to become collapsed // - or if the summary is about to toggle to become collapsed
// - or if there are fewEvents, meaning the child eventTiles are shown as-is // - or if there are fewEvents, meaning the child eventTiles are shown as-is
return ( return (
nextProps.events.length !== this.props.events.length || nextProps.events.length !== this.props.events.length ||
this.state.expanded || nextState.expanded ||
nextProps.events.length < this.props.threshold nextProps.events.length < this.props.threshold
); );
}, },
_toggleSummary: function() {
this.setState({
expanded: !this.state.expanded,
});
this.props.onToggle();
},
/** /**
* Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where * Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
* the sequences are ordered by `orderedTransitionSequences`. * the sequences are ordered by `orderedTransitionSequences`.
* @param {object[]} eventAggregates a map of transition sequence to array of user display names * @param {object[]} eventAggregates a map of transition sequence to array of user display names
* or user IDs. * or user IDs.
* @param {string[]} orderedTransitionSequences an array which is some ordering of * @param {string[]} orderedTransitionSequences an array which is some ordering of
* `Object.keys(eventAggregates)`. * `Object.keys(eventAggregates)`.
* @returns {ReactElement} a single <span> containing the textual summary of the aggregated * @returns {string} the textual summary of the aggregated events that occurred.
* events that occurred.
*/ */
_renderSummary: function(eventAggregates, orderedTransitionSequences) { _generateSummary: function(eventAggregates, orderedTransitionSequences) {
const summaries = orderedTransitionSequences.map((transitions) => { const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions]; const userNames = eventAggregates[transitions];
const nameList = this._renderNameList(userNames); const nameList = this._renderNameList(userNames);
@ -118,11 +102,7 @@ module.exports = createReactClass({
return null; return null;
} }
return ( return summaries.join(", ");
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
{ summaries.join(", ") }
</span>
);
}, },
/** /**
@ -208,7 +188,7 @@ module.exports = createReactClass({
* For a certain transition, t, describe what happened to the users that * For a certain transition, t, describe what happened to the users that
* underwent the transition. * underwent the transition.
* @param {string} t the transition type. * @param {string} t the transition type.
* @param {integer} userCount number of usernames * @param {number} userCount number of usernames
* @param {number} repeats the number of times the transition was repeated in a row. * @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition. * @returns {string} the written Human Readable equivalent of the transition.
*/ */
@ -288,19 +268,6 @@ module.exports = createReactClass({
return res; return res;
}, },
_renderAvatars: function(roomMembers) {
const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
return (
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
);
});
return (
<span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
{ avatars }
</span>
);
},
_getTransitionSequence: function(events) { _getTransitionSequence: function(events) {
return events.map(this._getTransition); return events.map(this._getTransition);
}, },
@ -396,22 +363,6 @@ module.exports = createReactClass({
render: function() { render: function() {
const eventsToRender = this.props.events; const eventsToRender = this.props.events;
const eventIds = eventsToRender.map((e) => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents;
let expandedEvents = null;
if (expanded) {
expandedEvents = this.props.children;
}
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{ expandedEvents }
</div>
);
}
// Map user IDs to an array of objects: // Map user IDs to an array of objects:
const userEvents = { const userEvents = {
@ -455,30 +406,14 @@ module.exports = createReactClass({
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2], (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
); );
let summaryContainer = null; const EventListSummary = sdk.getComponent("views.elements.EventListSummary");
if (!expanded) { return <EventListSummary
summaryContainer = ( events={this.props.events}
<div className="mx_EventTile_line"> threshold={this.props.threshold}
<div className="mx_EventTile_info"> onToggle={this.props.onToggle}
{ this._renderAvatars(avatarMembers) } startExpanded={this.props.startExpanded}
{ this._renderSummary(aggregate.names, orderedTransitionSequences) } children={this.props.children}
</div> summaryMembers={avatarMembers}
</div> summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
);
}
const toggleButton = (
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
{ expanded ? _t('collapse') : _t('expand') }
</div>
);
return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{ toggleButton }
{ summaryContainer }
{ expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null }
{ expandedEvents }
</div>
);
}, },
}); });

View file

@ -0,0 +1,27 @@
/*
Copyright 2019 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 {useState} from 'react';
// Hook to simplify toggling of a boolean state value
// Returns value, method to toggle boolean value and method to set the boolean value
export const useStateToggle = (initialValue) => {
const [value, setValue] = useState(Boolean(initialValue));
const toggleValue = () => {
setValue(!value);
};
return [value, toggleValue, setValue];
};

View file

@ -1131,6 +1131,8 @@
"Yes": "Yes", "Yes": "Yes",
"No": "No", "No": "No",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse",
"expand": "expand",
"Communities": "Communities", "Communities": "Communities",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",
@ -1193,8 +1195,6 @@
"%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes",
"%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times",
"%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes",
"collapse": "collapse",
"expand": "expand",
"Power level": "Power level", "Power level": "Power level",
"Custom level": "Custom level", "Custom level": "Custom level",
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",

View file

@ -162,7 +162,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -198,7 +198,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -246,7 +246,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -299,7 +299,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -358,7 +358,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -396,7 +396,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -447,7 +447,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -521,7 +521,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -568,7 +568,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -604,7 +604,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -632,7 +632,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -659,7 +659,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;
@ -684,7 +684,7 @@ describe('MemberEventListSummary', function() {
<MemberEventListSummary {...props} />, <MemberEventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_MemberEventListSummary_summary", instance, "mx_EventListSummary_summary",
); );
const summaryText = summary.innerText; const summaryText = summary.innerText;