diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 29b2f773dc..3b6547561e 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -20,7 +20,7 @@ var dis = require("../../dispatcher");
var sdk = require('../../index');
var MatrixClientPeg = require('../../MatrixClientPeg')
-var TruncatedList = require('../views/elements/TruncatedList.js');
+var MemberEventListSummary = require('../views/elements/MemberEventListSummary.js');
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@@ -287,54 +287,47 @@ module.exports = React.createClass({
var last = (i == lastShownEventIndex);
- // Wrap consecutive member events in a TruncatedList
- if (mxEv.getType() === 'm.room.member') {
+ var isMembershipChange = (e) =>
+ e.getType() === 'm.room.member'
+ && ['join', 'leave'].indexOf(e.event.content.membership) !== -1
+ && (!e.event.prev_content || e.event.content.membership !== e.event.prev_content.membership);
+
+ // Wrap consecutive member events in a ListSummary
+ if (isMembershipChange(mxEv)) {
// Prevent message continuations between truncations
prevEvent = null;
- let collapsedEvents = [mxEv];
+ let summarisedEvents = [mxEv];
i++;
for (;i < this.props.events.length; i++) {
let collapsedMxEv = this.props.events[i];
- if (collapsedMxEv.getType() !== 'm.room.member') {
+ if (!isMembershipChange(collapsedMxEv)) {
i--;
break;
}
- collapsedEvents.push(collapsedMxEv);
+ summarisedEvents.push(collapsedMxEv);
}
let ePrev = null;
- collapsedEvents = collapsedEvents.map(
- (e) => {
- let ret = this._getTilesForEvent(ePrev, e);
- ePrev = e;
- return ret;
+ let renderEvents = (events) => {
+ if (events.length === 0) {
+ return null;
}
- ).reduce((a,b) => a.concat(b));
-
- let overflowElement = (overflowCount, totalCount, toggleTruncate, isExpanded) => {
- if (isExpanded) {
- return (
-
- );
- }
- else {
- return (
-
- );
- }
- }
+ return events.map(
+ (e) => {
+ let ret = this._getTilesForEvent(ePrev, e);
+ ePrev = e;
+ return ret;
+ }
+ ).reduce((a,b) => a.concat(b));
+ };
ret.push(
-
- {collapsedEvents}
-
+
);
-
- wantTile = false;
+ continue;
}
if (wantTile) {
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js
new file mode 100644
index 0000000000..d0072fc71d
--- /dev/null
+++ b/src/components/views/elements/MemberEventListSummary.js
@@ -0,0 +1,228 @@
+/*
+Copyright 2016 OpenMarket Ltd
+
+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.
+*/
+const React = require('react');
+const MemberAvatar = require('../avatars/MemberAvatar.js');
+const dispatcher = require("../../../dispatcher");
+
+module.exports = React.createClass({
+ displayName: 'MemberEventListSummary',
+
+ propTypes: {
+ // An array of member events to summarise
+ events: React.PropTypes.array,
+ // The maximum number of names to show in either the join or leave summaries
+ summaryLength: React.PropTypes.number,
+ // The maximum number of avatars to display in the summary
+ avatarsMaxLength: React.PropTypes.number,
+ // The minimum number of events needed to trigger summarisation
+ threshold: React.PropTypes.number,
+ // The function to render events if they are not being summarised
+ renderEvents: React.PropTypes.function,
+ },
+
+ getInitialState: function() {
+ return {
+ expanded: false,
+ };
+ },
+
+ getDefaultProps: function() {
+ return {
+ summaryLength: 3,
+ threshold: 3,
+ avatarsMaxLength: 5
+ };
+ },
+
+ toggleSummary: function() {
+ this.setState({
+ expanded: !this.state.expanded,
+ });
+ },
+
+ getEventSenderName: function(ev) {
+ if (!ev) {
+ return 'undefined';
+ }
+ return ev.sender.name || ev.event.content.displayname || ev.getSender();
+ },
+
+ renderNameList: function(events) {
+ if (events.length === 0) {
+ return null;
+ }
+ let originalNumber = events.length;
+ events = events.slice(0, this.props.summaryLength);
+ let lastEvent = events.pop();
+
+ let names = events.map((ev) => {
+ return this.getEventSenderName(ev);
+ }).join(', ');
+
+ if (names.length === 0) {
+ return this.getEventSenderName(lastEvent);
+ }
+
+ // Special case the last name. ' and ' might be included later
+ // So you have two cases:
+ if (originalNumber <= this.props.summaryLength) {
+ // name1, name2 and name3
+ names += ' and ';
+ } else {
+ // name1, name2, name3 [and 100 others]
+ names += ', ';
+ }
+ return names + this.getEventSenderName(lastEvent);
+ },
+
+ renderSummary: function(joinEvents, leaveEvents) {
+ let joiners = this.renderNameList(joinEvents);
+ let remainingJoiners = joinEvents.length - this.props.summaryLength;
+
+ let leavers = this.renderNameList(leaveEvents);
+ let remainingLeavers = leaveEvents.length - this.props.summaryLength;
+
+ let joinSummary = null;
+
+ if (joiners) {
+ joinSummary = (
+
+ {joiners} {remainingJoiners > 0 ? 'and ' + remainingJoiners + ' others ':''}joined the room
+
+ );
+ }
+
+ let leaveSummary = '';
+
+ if (leavers) {
+ leaveSummary = (
+
+ {leavers} {remainingLeavers > 0 ? 'and ' + remainingLeavers + ' others ':''}left the room
+
+ );
+ }
+
+ return (
+
+ {joinSummary}{joinSummary && leaveSummary?'; ':''}
+ {leaveSummary}
+
+ );
+ },
+
+
+
+ renderAvatars: function(events) {
+
+ let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
+ let onClickAvatar = () => {
+ dispatcher.dispatch({
+ action: 'view_user',
+ member: e.sender,
+ });
+ };
+ return (
+
+ );
+ });
+
+ return (
+
+ {avatars}
+
+ );
+ },
+
+ render: function() {
+ let summary = null;
+
+ // Reorder events so that joins come before leaves
+ let eventsToRender = this.props.events;
+
+ // Filter out those who joined, then left
+ let filteredEvents = eventsToRender.filter(
+ (e) => {
+ return eventsToRender.filter(
+ (e2) => {
+ return e.getSender() === e2.getSender()
+ && e.event.content.membership !== e2.event.content.membership;
+ }
+ ).length === 0;
+ }
+ );
+
+ let joinAndLeft = (eventsToRender.length - filteredEvents.length) / 2;
+ if (joinAndLeft <= 0 || joinAndLeft % 1 !== 0) {
+ joinAndLeft = null;
+ }
+
+ let joinEvents = filteredEvents.filter((ev) => {
+ return ev.event.content.membership === 'join';
+ });
+
+ let leaveEvents = filteredEvents.filter((ev) => {
+ return ev.event.content.membership === 'leave';
+ });
+
+ let fewEvents = eventsToRender.length < this.props.threshold;
+ console.log(eventsToRender.length, joinEvents.length, leaveEvents.length, this.state.expanded, fewEvents);
+
+ let expanded = this.state.expanded || fewEvents;
+ let expandedEvents = null;
+
+ if (expanded) {
+ expandedEvents = this.props.renderEvents(eventsToRender);
+ }
+
+ let avatars = this.renderAvatars(joinEvents.concat(leaveEvents));
+
+ let toggleButton = null;
+ let summaryContainer = null;
+ if (!fewEvents) {
+ summary = this.renderSummary(joinEvents, leaveEvents);
+ toggleButton = (
+ {expanded?'collapse':'expand'}
+ );
+
+ summaryContainer = (
+
+
+
+ {avatars}
+
+
+ {summary}{joinAndLeft? '. ' + joinAndLeft + ' others joined and left' : ''}
+
+ {toggleButton}
+
+
+ );
+ }
+
+ return (
+
+ {summaryContainer}
+ {expandedEvents}
+
+ );
+ },
+});