Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Overhaul MELS to deal with causality, kicks, etc. #613

Merged
merged 28 commits into from
Jan 25, 2017

Conversation

lukebarnard1
Copy link
Contributor

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.

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.
Copy link
Member

@richvdh richvdh left a 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
Copy link
Member

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.

Copy link
Contributor Author

@lukebarnard1 lukebarnard1 Jan 16, 2017

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) {
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

@lukebarnard1 lukebarnard1 Jan 13, 2017

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?

Copy link
Contributor Author

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) {
Copy link
Member

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;
}

Copy link
Contributor Author

@lukebarnard1 lukebarnard1 Jan 16, 2017

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)) {
Copy link
Member

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.

Copy link
Contributor Author

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?

@richvdh richvdh assigned lukebarnard1 and unassigned richvdh Jan 13, 2017
Luke Barnard and others added 9 commits January 13, 2017 16:40
…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 ...
@richvdh richvdh assigned richvdh and unassigned lukebarnard1 Jan 17, 2017
Copy link
Member

@richvdh richvdh left a 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() {
Copy link
Member

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);
Copy link
Member

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).

Copy link
Contributor Author

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) => {
Copy link
Member

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?

Copy link
Contributor Author

@lukebarnard1 lukebarnard1 Jan 18, 2017

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;
Copy link
Member

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) => {
Copy link
Member

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) {
Copy link
Member

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).

Copy link
Contributor Author

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;
Copy link
Member

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;
Copy link
Member

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) {
Copy link
Member

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";
Copy link
Member

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.

@richvdh richvdh assigned lukebarnard1 and unassigned richvdh Jan 17, 2017
@lukebarnard1 lukebarnard1 assigned richvdh and unassigned lukebarnard1 Jan 24, 2017
};
});
// Override random event ID
e.event.event_id = eventId;
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Copy link
Member

@richvdh richvdh left a 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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still .only ? whyyyyyyyyyyyy?

Copy link
Contributor Author

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:
Copy link
Member

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.
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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?

Copy link
Member

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.

Copy link
Contributor Author

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lies

@richvdh richvdh assigned lukebarnard1 and unassigned richvdh Jan 24, 2017
@lukebarnard1 lukebarnard1 assigned richvdh and unassigned lukebarnard1 Jan 25, 2017
@richvdh
Copy link
Member

richvdh commented Jan 25, 2017 via email

@lukebarnard1 lukebarnard1 assigned lukebarnard1 and unassigned richvdh Jan 25, 2017
@lukebarnard1 lukebarnard1 assigned richvdh and unassigned lukebarnard1 Jan 25, 2017
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@richvdh richvdh merged commit 9cadc2f into develop Jan 25, 2017
@lukebarnard1
Copy link
Contributor Author

image

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants