Merge pull request #544 from matrix-org/luke/feature-truncate-m-room-member-events

Truncate consecutive member events
This commit is contained in:
Richard van der Hoff 2016-11-11 11:01:47 +00:00 committed by GitHub
commit 00ecff7497
5 changed files with 276 additions and 12 deletions

View File

@ -105,6 +105,8 @@ import views$elements$EditableTextContainer from './components/views/elements/Ed
views$elements$EditableTextContainer && (module.exports.components['views.elements.EditableTextContainer'] = views$elements$EditableTextContainer);
import views$elements$EmojiText from './components/views/elements/EmojiText';
views$elements$EmojiText && (module.exports.components['views.elements.EmojiText'] = views$elements$EmojiText);
import views$elements$MemberEventListSummary from './components/views/elements/MemberEventListSummary';
views$elements$MemberEventListSummary && (module.exports.components['views.elements.MemberEventListSummary'] = views$elements$MemberEventListSummary);
import views$elements$PowerSelector from './components/views/elements/PowerSelector';
views$elements$PowerSelector && (module.exports.components['views.elements.PowerSelector'] = views$elements$PowerSelector);
import views$elements$ProgressBar from './components/views/elements/ProgressBar';

View File

@ -229,6 +229,7 @@ module.exports = React.createClass({
_getEventTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
this.eventNodes = {};
@ -275,6 +276,11 @@ module.exports = React.createClass({
this.currentGhostEventId = null;
}
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);
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
var wantTile = true;
@ -286,6 +292,42 @@ module.exports = React.createClass({
var last = (i == lastShownEventIndex);
// Wrap consecutive member events in a ListSummary
if (isMembershipChange(mxEv)) {
let summarisedEvents = [mxEv];
i++;
for (;i < this.props.events.length; i++) {
let collapsedMxEv = this.props.events[i];
if (!isMembershipChange(collapsedMxEv)) {
i--;
break;
}
summarisedEvents.push(collapsedMxEv);
}
// At this point, i = this.props.events.length OR i = the index of the last
// MembershipChange in a sequence of MembershipChanges
let eventTiles = summarisedEvents.map(
(e) => {
let ret = this._getTilesForEvent(prevEvent, e);
prevEvent = e;
return ret;
}
).reduce((a,b) => a.concat(b));
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<MemberEventListSummary events={summarisedEvents}>
{eventTiles}
</MemberEventListSummary>
);
continue;
}
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up

View File

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var Avatar = require('../../../Avatar');
var sdk = require("../../../index");
const dispatcher = require("../../../dispatcher");
module.exports = React.createClass({
displayName: 'MemberAvatar',
@ -27,14 +28,19 @@ module.exports = React.createClass({
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string
resizeMethod: React.PropTypes.string,
// The onClick to give the avatar
onClick: React.PropTypes.function,
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
viewUserOnClick: React.PropTypes.boolean,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop'
resizeMethod: 'crop',
viewUserOnClick: false,
}
},
@ -63,11 +69,20 @@ module.exports = React.createClass({
render: function() {
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
var {member, ...otherProps} = this.props;
var {member, onClick, ...otherProps} = this.props;
if (this.props.viewUserOnClick) {
onClick = () => {
dispatcher.dispatch({
action: 'view_user',
member: this.props.member,
});
}
}
return (
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
idName={member.userId} url={this.state.imageUrl} />
idName={member.userId} url={this.state.imageUrl} onClick={onClick}/>
);
}
});

View File

@ -0,0 +1,212 @@
/*
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.
*/
import React from 'react';
const MemberAvatar = require('../avatars/MemberAvatar.js');
module.exports = React.createClass({
displayName: 'MemberEventListSummary',
propTypes: {
// An array of member events to summarise
events: React.PropTypes.array.isRequired,
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
// 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,
},
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(', ');
let lastName = this._getEventSenderName(lastEvent);
if (names.length === 0) {
// special-case for a single event
return lastName;
}
let remaining = originalNumber - this.props.summaryLength;
if (remaining > 0) {
// name1, name2, name3, and 100 others
return names + ', ' + lastName + ', and ' + remaining + ' others';
} else {
// name1, name2 and name3
return names + ' and ' + lastName;
}
},
_renderSummary: function(joinEvents, leaveEvents) {
let joiners = this._renderNameList(joinEvents);
let leavers = this._renderNameList(leaveEvents);
let joinSummary = null;
if (joiners) {
joinSummary = (
<span>
{joiners} joined the room
</span>
);
}
let leaveSummary = null;
if (leavers) {
leaveSummary = (
<span>
{leavers} left the room
</span>
);
}
return (
<span>
{joinSummary}{joinSummary && leaveSummary?'; ':''}
{leaveSummary}
</span>
);
},
_renderAvatars: function(events) {
let avatars = events.slice(0, this.props.avatarsMaxLength).map((e) => {
return (
<MemberAvatar
key={e.getId()}
member={e.sender}
width={14}
height={14}
/>
);
});
return (
<span>
{avatars}
</span>
);
},
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;
let expanded = this.state.expanded || fewEvents;
let expandedEvents = null;
if (expanded) {
expandedEvents = this.props.children;
}
let avatars = this._renderAvatars(joinEvents.concat(leaveEvents));
let toggleButton = null;
let summaryContainer = null;
if (!fewEvents) {
summary = this._renderSummary(joinEvents, leaveEvents);
toggleButton = (
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
{expanded ? 'collapse' : 'expand'}
</a>
);
let plural = (joinEvents.length + leaveEvents.length > 0) ? 'others' : 'users';
let noun = (joinAndLeft === 1 ? 'user' : plural);
summaryContainer = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_MemberEventListSummary_avatars">
{avatars}
</span>
<span className="mx_TextualEvent mx_MemberEventListSummary_summary">
{summary}{joinAndLeft? '. ' + joinAndLeft + ' ' + noun + ' joined and left' : ''}
</span>&nbsp;
{toggleButton}
</div>
</div>
);
}
return (
<div className="mx_MemberEventListSummary">
{summaryContainer}
{expandedEvents}
</div>
);
},
});

View File

@ -348,13 +348,6 @@ module.exports = React.createClass({
</span>;
},
onMemberAvatarClick: function(event) {
dispatcher.dispatch({
action: 'view_user',
member: this.props.mxEvent.sender,
});
},
onSenderProfileClick: function(event) {
var mxEvent = this.props.mxEvent;
dispatcher.dispatch({
@ -443,7 +436,7 @@ module.exports = React.createClass({
<div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender}
width={avatarSize} height={avatarSize}
onClick={ this.onMemberAvatarClick }
viewUserOnClick={true}
/>
</div>
);