Files
ThreadNet-Web/src/components/views/elements/MemberEventListSummary.js
T

417 lines
15 KiB
JavaScript
Raw Normal View History

2016-11-09 16:03:35 +00:00
/*
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';
2016-11-09 16:03:35 +00:00
const MemberAvatar = require('../avatars/MemberAvatar.js');
module.exports = React.createClass({
displayName: 'MemberEventListSummary',
propTypes: {
// An array of member events to summarise
2016-11-10 14:09:40 +00:00
events: React.PropTypes.array.isRequired,
2016-11-10 14:12:45 +00:00
// An array of EventTiles to render when expanded
children: React.PropTypes.array.isRequired,
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
2016-11-09 16:03:35 +00:00
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: 1,
2016-11-09 16:03:35 +00:00
threshold: 3,
avatarsMaxLength: 5,
2016-11-09 16:03:35 +00:00
};
},
2017-01-16 15:12:00 +01:00
shouldComponentUpdate: function(nextProps, nextState) {
// 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
);
},
2016-11-10 14:12:05 +00:00
_toggleSummary: function() {
2016-11-09 16:03:35 +00:00
this.setState({
expanded: !this.state.expanded,
});
},
/**
* Render the JSX 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.
*/
_renderSummary: function(eventAggregates, orderedTransitionSequences) {
let summaries = orderedTransitionSequences.map((transitions) => {
let userNames = eventAggregates[transitions];
let nameList = this._renderNameList(userNames);
let plural = userNames.length > 1;
let splitTransitions = transitions.split(',');
// Some neighbouring transitions are common, so canonicalise some into "pair" transitions
let canonicalTransitions = this._getCanonicalTransitions(splitTransitions);
// Transform into consecutive repetitions of the same transition (like 5 consecutive 'joined_and_left's)
let truncatedTransitions = this._getTruncatedTransitions(canonicalTransitions);
let descs = truncatedTransitions.map((t) => {
return this._getDescriptionForTransition(t.transitionType, plural, t.repeats);
});
let desc = this._renderCommaSeparatedList(descs);
return nameList + " " + desc;
});
if (!summaries) {
return null;
}
return (
<span>
{summaries.join(", ")}
</span>
);
},
/**
* @param {string[]} users an array of user display names or user IDs.
* @returns {string} a comma-separated list that ends with "and [n] others" if there are
* more items in `users` than `this.props.summaryLength`, which is the number of names
* included before "and [n] others".
*/
_renderNameList: function(users) {
return this._renderCommaSeparatedList(users, this.props.summaryLength);
},
/**
* Canonicalise an array of transitions into an array of transitions and how many times
* they are repeated consecutively.
*
* An array of 123 "joined_and_left" transitions, would result in:
* ```
* [{
* transitionType: "joined_and_left"
* repeats: 123
* }, ... ]
* ```
* @param {string[]} transitions the array of transitions to transform.
* @returns {object[]} an array of truncated transitions.
*/
_getCanonicalTransitions: function(transitions) {
let modMap = {
'joined' : {
'after' : 'left',
'newTransition' : 'joined_and_left',
},
'left' : {
'after' : 'joined',
'newTransition' : 'left_and_joined',
},
// $currentTransition : {
// 'after' : $nextTransition,
// 'newTransition' : 'new_transition_type',
// },
};
const res = [];
for (let i = 0; i < transitions.length; i++) {
let t = transitions[i];
let t2 = transitions[i + 1];
let transition = t;
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
transition = modMap[t].newTransition;
i++;
}
res.push(transition);
}
return res;
},
/**
* Transform an array of transitions into an array of transitions and how many times
* they are repeated consecutively.
*
* An array of 123 "joined_and_left" transitions, would result in:
* ```
* [{
* transitionType: "joined_and_left"
* repeats: 123
* }, ... ]
* ```
* @param {string[]} transitions the array of transitions to transform.
* @returns {object[]} an array of truncated transitions.
*/
_getTruncatedTransitions: function(transitions) {
let res = [];
for (let i = 0; i < transitions.length; i++) {
if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
res[res.length - 1].repeats += 1;
} else {
res.push({
transitionType: transitions[i],
repeats: 1,
});
}
}
return res;
},
/**
* For a certain transition, t, describe what happened to the users that
* underwent the transition.
* @param {string} t the transition type.
* @param {boolean} plural whether there were multiple users undergoing the same transition.
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written English equivalent of the transition.
*/
_getDescriptionForTransition(t, plural, repeats) {
let beConjugated = plural ? "were" : "was";
let invitation = "their invitation" + (plural || (repeats > 1) ? "s" : "");
let res = null;
let map = {
"joined": "joined",
"left": "left",
"joined_and_left": "joined and left",
"left_and_joined": "left and rejoined",
"invite_reject": "rejected " + invitation,
"invite_withdrawal": "had " + invitation + " withdrawn",
"invited": beConjugated + " invited",
"banned": beConjugated + " banned",
"unbanned": beConjugated + " unbanned",
"kicked": beConjugated + " kicked",
};
if (Object.keys(map).includes(t)) {
res = map[t] + (repeats > 1 ? " " + repeats + " times" : "" );
2016-11-16 14:42:30 +00:00
}
return res;
},
/**
* Constructs a written English string representing `items`, with an optional limit on the number
* of items included in the result. If specified and if the length of `items` is greater than the
* limit, the string "and n others" will be appended onto the result.
* If `items` is empty, returns the empty string. If there is only one item, return it.
* @param {string[]} items the items to construct a string from.
* @param {number?} itemLimit the number by which to limit the list.
* @returns {string} a string constructed by joining `items` with a comma between each
* item, but with the last item appended as " and [lastItem]".
*/
_renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0);
if (items.length === 0) {
return "";
} else if (items.length === 1) {
return items[0];
} else if (remaining) {
items = items.slice(0, itemLimit);
const other = " other" + (remaining > 1 ? "s" : "");
return items.join(', ') + ' and ' + remaining + other;
} else {
let last = items.pop();
return items.join(', ') + ' and ' + last;
}
2016-11-09 16:03:35 +00:00
},
_renderAvatars: function(roomMembers) {
let avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
2016-11-09 16:03:35 +00:00
return (
<MemberAvatar key={m.userId} member={m} width={14} height={14} />
2016-11-09 16:03:35 +00:00
);
});
return (
<span>
{avatars}
</span>
);
},
_getTransitionSequence: function(events) {
return events.map(this._getTransition);
},
/**
* Enumerate a given membership event, `e`, where `getContent().membership` has
* changed for each transition allowed by the Matrix protocol. This attempts to
* enumerate the membership changes that occur in `../../../TextForEvent.js`.
* @param {MatrixEvent} e the membership change event to enumerate.
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
_getTransition: function(e) {
switch (e.mxEvent.getContent().membership) {
case 'invite': return 'invited';
case 'ban': return 'banned';
case 'join': return 'joined';
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_reject';
default: return 'left';
}
}
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return 'invite_withdrawal';
case 'ban': return 'unbanned';
case 'join': return 'kicked';
default: return 'left';
}
default: return null;
}
},
render: function() {
let eventsToRender = this.props.events;
2016-11-09 16:03:35 +00:00
let fewEvents = eventsToRender.length < this.props.threshold;
let expanded = this.state.expanded || fewEvents;
let expandedEvents = null;
2016-11-09 16:03:35 +00:00
if (expanded) {
expandedEvents = this.props.children;
2016-11-09 16:03:35 +00:00
}
if (fewEvents) {
return (
<div className="mx_MemberEventListSummary">
{expandedEvents}
2016-11-09 16:03:35 +00:00
</div>
);
}
// Map user IDs to an array of objects:
let userEvents = {
// $userId : [{
// // The original event
// mxEvent: e,
// // The display name of the user (if not, then user ID)
// displayName: e.target.name || userId,
// // The original index of the event in this.props.events
// index: index,
// }]
};
let avatarMembers = [];
eventsToRender.forEach((e, index) => {
2017-01-11 17:03:14 +00:00
const userId = e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
avatarMembers.push(e.target);
}
userEvents[userId].push({
mxEvent: e,
displayName: e.target.name || userId,
index: index,
});
});
// A map of aggregate type to arrays of display names. Each aggregate type
// is a comma-delimited string of transitions, e.g. "joined,left,kicked".
// The array of display names is the array of users who went through that
// sequence during eventsToRender.
let aggregate = {
// $aggregateType : []:string
};
// A map of aggregate types to the indices that order them (the index of
// the first event for a given transition sequence)
let aggregateIndices = {
// $aggregateType : int
};
let users = Object.keys(userEvents);
users.forEach(
(userId) => {
let firstEvent = userEvents[userId][0];
let displayName = firstEvent.displayName;
let seq = this._getTransitionSequence(userEvents[userId]);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
if (aggregate[seq].indexOf(displayName) === -1) {
aggregate[seq].push(displayName);
}
if (aggregateIndices[seq] === -1 || firstEvent.index < aggregateIndices[seq]) {
aggregateIndices[seq] = firstEvent.index;
}
}
);
// Sort types by order of lowest event index within sequence
let orderedTransitionSequences = Object.keys(aggregate).sort((seq1, seq2) => aggregateIndices[seq1] > aggregateIndices[seq2]);
let avatars = this._renderAvatars(avatarMembers);
let summary = this._renderSummary(aggregate, orderedTransitionSequences);
let toggleButton = (
<a className="mx_MemberEventListSummary_toggle" onClick={this._toggleSummary}>
{expanded ? 'collapse' : 'expand'}
</a>
);
let 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}
</span>&nbsp;
{toggleButton}
</div>
</div>
);
2016-11-09 16:03:35 +00:00
return (
<div className="mx_MemberEventListSummary">
{summaryContainer}
{expandedEvents}
</div>
);
},
});