diff --git a/res/css/_components.scss b/res/css/_components.scss index f627fe3a29..561b1b4820 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -88,12 +88,12 @@ @import "./views/elements/_Dropdown.scss"; @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; +@import "./views/elements/_EventListSummary"; @import "./views/elements/_Field.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InteractiveTooltip.scss"; @import "./views/elements/_ManageIntegsButton.scss"; -@import "./views/elements/_MemberEventListSummary.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ReplyThread.scss"; diff --git a/res/css/views/elements/_MemberEventListSummary.scss b/res/css/views/elements/_EventListSummary.scss similarity index 75% rename from res/css/views/elements/_MemberEventListSummary.scss rename to res/css/views/elements/_EventListSummary.scss index 02ecb5d84a..99a5c06a5f 100644 --- a/res/css/views/elements/_MemberEventListSummary.scss +++ b/res/css/views/elements/_EventListSummary.scss @@ -14,28 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberEventListSummary { +.mx_EventListSummary { position: relative; } -.mx_TextualEvent.mx_MemberEventListSummary_summary { +.mx_TextualEvent.mx_EventListSummary_summary { font-size: 14px; display: inline-flex; } -.mx_MemberEventListSummary_avatars { +.mx_EventListSummary_avatars { display: inline-block; margin-right: 8px; padding-top: 8px; line-height: 12px; } -.mx_MemberEventListSummary_avatars .mx_BaseAvatar { +.mx_EventListSummary_avatars .mx_BaseAvatar { margin-right: -4px; cursor: pointer; } -.mx_MemberEventListSummary_toggle { +.mx_EventListSummary_toggle { color: $accent-color; cursor: pointer; float: right; @@ -43,29 +43,29 @@ limitations under the License. margin-top: 8px; } -.mx_MemberEventListSummary_line { +.mx_EventListSummary_line { border-bottom: 1px solid $primary-hairline-color; margin-left: 63px; line-height: 30px; } .mx_MatrixChat_useCompactLayout { - .mx_MemberEventListSummary { + .mx_EventListSummary { font-size: 13px; .mx_EventTile_line { line-height: 20px; } } - .mx_MemberEventListSummary_line { + .mx_EventListSummary_line { line-height: 22px; } - .mx_MemberEventListSummary_toggle { + .mx_EventListSummary_toggle { margin-top: 3px; } - .mx_TextualEvent.mx_MemberEventListSummary_summary { + .mx_TextualEvent.mx_EventListSummary_summary { font-size: 13px; } } diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.js new file mode 100644 index 0000000000..d6971334d4 --- /dev/null +++ b/src/components/views/elements/EventListSummary.js @@ -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"> </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; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index ba31eb5a38..98adbb2e5c 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -19,9 +19,9 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; +import sdk from "../../../index"; module.exports = createReactClass({ displayName: 'MemberEventListSummary', @@ -43,12 +43,6 @@ module.exports = createReactClass({ startExpanded: PropTypes.bool, }, - getInitialState: function() { - return { - expanded: Boolean(this.props.startExpanded), - }; - }, - getDefaultProps: function() { return { summaryLength: 1, @@ -57,37 +51,27 @@ module.exports = createReactClass({ }; }, - shouldComponentUpdate: function(nextProps, nextState) { + shouldComponentUpdate: function(nextProps) { // Update if // - 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 there are fewEvents, meaning the child eventTiles are shown as-is return ( nextProps.events.length !== this.props.events.length || - this.state.expanded || nextState.expanded || 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`. * @param {object[]} eventAggregates a map of transition sequence to array of user display names * or user IDs. * @param {string[]} orderedTransitionSequences an array which is some ordering of * `Object.keys(eventAggregates)`. - * @returns {ReactElement} a single <span> containing the textual summary of the aggregated - * events that occurred. + * @returns {string} the textual summary of the aggregated events that occurred. */ - _renderSummary: function(eventAggregates, orderedTransitionSequences) { + _generateSummary: function(eventAggregates, orderedTransitionSequences) { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this._renderNameList(userNames); @@ -118,11 +102,7 @@ module.exports = createReactClass({ return null; } - return ( - <span className="mx_TextualEvent mx_MemberEventListSummary_summary"> - { summaries.join(", ") } - </span> - ); + return summaries.join(", "); }, /** @@ -208,7 +188,7 @@ module.exports = createReactClass({ * For a certain transition, t, describe what happened to the users that * underwent the transition. * @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. * @returns {string} the written Human Readable equivalent of the transition. */ @@ -288,19 +268,6 @@ module.exports = createReactClass({ 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) { return events.map(this._getTransition); }, @@ -396,22 +363,6 @@ module.exports = createReactClass({ render: function() { 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: const userEvents = { @@ -455,30 +406,14 @@ module.exports = createReactClass({ (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2], ); - let summaryContainer = null; - if (!expanded) { - summaryContainer = ( - <div className="mx_EventTile_line"> - <div className="mx_EventTile_info"> - { this._renderAvatars(avatarMembers) } - { this._renderSummary(aggregate.names, orderedTransitionSequences) } - </div> - </div> - ); - } - 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"> </div> : null } - { expandedEvents } - </div> - ); + const EventListSummary = sdk.getComponent("views.elements.EventListSummary"); + return <EventListSummary + events={this.props.events} + threshold={this.props.threshold} + onToggle={this.props.onToggle} + startExpanded={this.props.startExpanded} + children={this.props.children} + summaryMembers={avatarMembers} + summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />; }, }); diff --git a/src/hooks/useStateToggle.js b/src/hooks/useStateToggle.js new file mode 100644 index 0000000000..58cf123bfb --- /dev/null +++ b/src/hooks/useStateToggle.js @@ -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]; +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02d1d0e8d6..f7016d7fad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1131,6 +1131,8 @@ "Yes": "Yes", "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.", + "collapse": "collapse", + "expand": "expand", "Communities": "Communities", "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", @@ -1193,8 +1195,6 @@ "%(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|one": "%(oneUser)smade no changes", - "collapse": "collapse", - "expand": "expand", "Power level": "Power 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.", diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 09a4739f06..db86b2ffab 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -162,7 +162,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -198,7 +198,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -246,7 +246,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -299,7 +299,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -358,7 +358,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -396,7 +396,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -447,7 +447,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -521,7 +521,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -568,7 +568,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -604,7 +604,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -632,7 +632,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -659,7 +659,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText; @@ -684,7 +684,7 @@ describe('MemberEventListSummary', function() { <MemberEventListSummary {...props} />, ); const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_MemberEventListSummary_summary", + instance, "mx_EventListSummary_summary", ); const summaryText = summary.innerText;