-
-
Notifications
You must be signed in to change notification settings - Fork 827
Overhaul MELS to deal with causality, kicks, etc. #613
Conversation
The MELS can now deal with arbitrary sequences of transitions per user, where a transition is a change in membership. A transition can be joined, left, invite_reject, invite_withdrawal, invited, banned, unbanned or kicked. Repeated segments (modulo 1 and 2), such as joined,left,joined,left,joined will be handled and will be rendered as " ... and 10 others joined and left 2 times and then joined". The repeated segments are assumed to be at the beginning of the sequence. This could be improved to handle arbitrary repeated sequences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs some tests. There are lots of edge cases here, and it's going to be very easy to break one edge case while fixing another and not notice it. Given most of the logic is just generating a string, it should be easy enough to do without having to deal with React. You might consider a MemberEventListSummaryGenerator
which takes the config properties in its constructor and has a generate
method which takes the list of events and returns a (string) summary?
The ordering of methods seems a bit arbitrary. Where possible, it's nice to be able to read a file from top to bottom with higher-level functions earlier, and the functions they call later. So, for example, _renderCommaSeparatedList
should be after all the functions that call it.
aggregate[seq] = []; | ||
} | ||
|
||
// Assumes display names are unique |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's an awful assumption. Can you aggregate by userId here, and then turn into displayname later by doing a room.getMember(userId).name
later? Not sure how well that will cope with people who leave, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, using target.name
works. Well it works as much as EventTiles
already do.
@@ -157,6 +191,32 @@ module.exports = React.createClass({ | |||
); | |||
}, | |||
|
|||
_getTransition: function(e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldComponentUpdate
is now in the middle of the render logic. suggest moving it to the top. Normal ordering is:
- react lifecycle methods
- internal methods
render
let joinedAndLeft = 0; | ||
let senders = Object.keys(userEvents); | ||
senders.forEach( | ||
// A map of agregate type to arrays of display names. Each aggregate type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't (aiui) preserve ordering. Per element-hq/element-web#2856 (comment), I am unconvinced that is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I think it will suffice to order by the first event of each user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if (remaining > 0) { | ||
// name1, name2, name3, and 100 others | ||
return names + ', ' + lastName + ', and ' + remaining + ' others'; | ||
_renderCommaSeparatedList(items, disableAnd) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than disableAnd
, it might be preferable to have a itemLimit
param, and move the 'and x others' logic in here - this making it reusable.
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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm!
|
||
for (let modulus = 1; modulus <= 2; modulus++) { | ||
// Sequences that are repeating through modulus transitions will be truncated | ||
if (this._isRepeatedSequence(describedTransitions, modulus)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that looking for a pattern repeating throughout the whole of the transitions is the right approach. My understanding of matthew's intentions was that we would look specifically for pairs of 'join, leave' or 'leave, join' and treat them as special transitions. YMMV though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you push the transitions "joined","left" together to form "joined_and_left" as a transition, say, then you could reduce the problem from modulo 2 to modulo 1. Then you could just look for possibly multiple contiguous blocks of transitions during any part of the sequence (instead of just the beginning). The only advantage the current implementation has over that is that it works with transitions in general. So a repeated "joined","kicked","joined","kicked" would be compressed. Otherwise you'd have to special case a "joined" next to a "kicked" as well...
But I'm guessing join/leave/join/leave is so common that it warrants a special transition?
…ransitions Transition pairs joined,left and left,joined are now transformed into single meta-transitions "joined_and_left" and "left_and_joined" respectively. These are described as "joined and left", "left and rejoined". Treat consecutive sequences of transitions as repetitions, and handle any arbitrary repetitions of transitions: ...,joined,left,joined,left,joined,left,... is canonicalised into ...,joined_and_left, joined_and_left, joined_and_left,... which is truncated and described as ... , joined and left 3 times, ... This also works if there are multiple consecutive sequences separated by other transitions: ..., banned, banned, banned, joined, unbanned, unbanned, unbanned,... becomes ... was banned 3 times, joined, was unbanned 3 times ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work on the tests! I still think it would have been easier to factor out a separate non-react thing which generates the summaries, but this is fine.
Generally looking pretty good!
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); | ||
|
||
const testUtils = require('../../../test-utils'); | ||
describe.only('MemberEventListSummary', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why .only
?
testUtils.beforeEach(this); | ||
sandbox = testUtils.stubClient(); | ||
parentDiv = document.createElement('div'); | ||
document.body.appendChild(parentDiv); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you need to remove this at the end of the test.
(In fact, there is no need to add it at all unless you want to observe it in the test browser or interact with it via the browser - react will render happily to a detached DOM node and it will probably be faster).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wfm
}); | ||
}; | ||
|
||
const generateMembershipEvent = (eventId, parameters) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's something very similar to this in test-utils
(mkMembership
). Could you use that to save reinventing this wheel?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With some adjustments, that sounds good 👍
userId = userIdTemplate.replace('$', i); | ||
events.forEach((e) => { | ||
e.userId = userId; | ||
return e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
redundant?
let sandbox; | ||
let parentDiv; | ||
|
||
const generateTiles = (events) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add some comments to these helper functions?
aggregateIndices[seq] = -1; | ||
} | ||
|
||
if (aggregate[seq].indexOf(displayName) === -1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if two different Matthews have the same aggregate sequence, then we ought to show it as "Matthew and one other joined", not just "Matthew joined". (IOW: you need to remove this condition).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that works.
_renderSummary: function(eventAggregates, orderedTransitionSequences) { | ||
let summaries = orderedTransitionSequences.map((transitions) => { | ||
let nameList = this._renderNameList(eventAggregates[transitions]); | ||
let plural = eventAggregates[transitions].length > 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plural what?
I think this would be clearer if you factored out let userNames = eventAggregates[transitions]
and used it here and above.
// movement is nil, then neither joinSummary nor leaveSummary will be | ||
// truthy, so return null. | ||
if (!joinSummary && !leaveSummary) { | ||
let repeats = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these look redundant?
_renderSummary: function(joinEvents, leaveEvents) { | ||
let joiners = this._renderNameList(joinEvents); | ||
let leavers = this._renderNameList(leaveEvents); | ||
_getDescriptionForTransition(t, plural, repeats) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could do with some more comments explaining what these helpers do, and what their params are. For example, the fact that plural
is a boolean flag meaning that more than one user was affected would be very useful knowledge here.
let joiners = this._renderNameList(joinEvents); | ||
let leavers = this._renderNameList(leaveEvents); | ||
_getDescriptionForTransition(t, plural, repeats) { | ||
let beConjugated = plural ? "were" : "was"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is going to be hilarious to i18nify.
…matrix-org/matrix-react-sdk into luke/fix-join-part-collapsing-causality Conflicts: src/components/views/elements/MemberEventListSummary.js
Conflicts: src/components/structures/MessagePanel.js
}; | ||
}); | ||
// Override random event ID | ||
e.event.event_id = eventId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this necessary, ooi?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because when the tests for expanded events are done, the shallow equality tests require a list of React instances (
<div className="event_tile" key="event0">Expanded membership</div>, |
And the keys of these must match those generated in generateTiles
, so they need to be constant, not random.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. Could you add this as a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some remaining grumbling, mostly about comments
@@ -4,12 +4,14 @@ const ReactDOM = require("react-dom"); | |||
const ReactTestUtils = require('react-addons-test-utils'); | |||
const sdk = require('matrix-react-sdk'); | |||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); | |||
const jssdk = require('matrix-js-sdk'); | |||
const MatrixEvent = jssdk.MatrixEvent; | |||
|
|||
const testUtils = require('../../../test-utils'); | |||
describe.only('MemberEventListSummary', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still .only ? whyyyyyyyyyyyy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, I'll get rid - it was just to isolate the test so that the others wouldn't run
* 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lies
* }, ... ] | ||
* ``` | ||
* @param {string[]} transitions the array of transitions to transform. | ||
* @returns {object[]} an array of truncated transitions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lies
}, | ||
|
||
/** | ||
* Enumerate a given membership event, `e`, where `getContent().membership` has |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this description. How can you enumerate one thing?
You keep using that word. I do not think it means what you think it means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I meant 'label', I'll use that instead
* ``` | ||
* @param {string[]} transitions the array of transitions to transform. | ||
* @returns {object[]} an array of truncated transitions. | ||
*/ | ||
_getTruncatedTransitions: function(transitions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a poor name for this. It implies that you're dropping transitions off the list, which isn't the case.
How about something like _coalesceRepeatedTransitions
or sth?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Truncated" appears in other places in this file too (including in the jsdoc). Remember to update them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed that truncated is poor and that to coalesce the events sounds better.
|
||
/** | ||
* Canonicalise an array of transitions into an array of transitions and how many times | ||
* they are repeated consecutively. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lies
Ok. You can also set KARMA_TEST_FILE in the environment to limit the
runner to one file without having to remember to fix the source.
…On 25/01/17 09:12, Luke Barnard wrote:
***@***.**** commented on this pull request.
------------------------------------------------------------------------
In test/components/views/elements/MemberEventListSummary-test.js
<#613>:
> @@ -4,12 +4,14 @@ const ReactDOM = require("react-dom");
const ReactTestUtils = require('react-addons-test-utils');
const sdk = require('matrix-react-sdk');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
+const jssdk = require('matrix-js-sdk');
+const MatrixEvent = jssdk.MatrixEvent;
const testUtils = require('../../../test-utils');
describe.only('MemberEventListSummary', function() {
oops, I'll get rid - it was just to isolate the test so that the
others wouldn't run
—
You are receiving this because you were assigned.
Reply to this email directly, view it on GitHub
<#613>, or mute the
thread
<https://github.com/notifications/unsubscribe-auth/ABU1VNH6e46qfCw0J1gEpzxwASWIs-Caks5rVxHzgaJpZM4LiHIv>.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
The MELS can now deal with arbitrary sequences of transitions per user, where a transition is a change in membership. A transition can be joined, left, invite_reject, invite_withdrawal, invited, banned, unbanned or kicked.
Repeated segments (modulo 1 and 2), such as joined,left,joined,left,joined will be handled and will be rendered as " ... and 10 others joined and left 2 times and then joined". The repeated segments are assumed to be at the beginning of the sequence. This could be improved to handle arbitrary repeated sequences.