Consolidate RedactionGrouper and HiddenEventGrouper into MELS (#7739)

This commit is contained in:
Michael Telatynski 2022-02-09 10:51:12 +00:00 committed by GitHub
parent 59cdd3edc0
commit 714136d4f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 750 additions and 887 deletions

View file

@ -141,7 +141,7 @@
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_GenericEventListSummary.scss";
@import "./views/elements/_EventTilePreview.scss"; @import "./views/elements/_EventTilePreview.scss";
@import "./views/elements/_ExternalLink.scss"; @import "./views/elements/_ExternalLink.scss";
@import "./views/elements/_FacePile.scss"; @import "./views/elements/_FacePile.scss";

View file

@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_EventListSummary { .mx_GenericEventListSummary {
position: relative; position: relative;
} }
.mx_TextualEvent.mx_EventListSummary_summary { .mx_TextualEvent.mx_GenericEventListSummary_summary {
font-size: $font-14px; font-size: $font-14px;
display: inline-flex; display: inline-flex;
} }
.mx_EventListSummary_avatars { .mx_GenericEventListSummary_avatars {
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
padding-top: 8px; padding-top: 8px;
line-height: $font-12px; line-height: $font-12px;
} }
.mx_EventListSummary_avatars .mx_BaseAvatar { .mx_GenericEventListSummary_avatars .mx_BaseAvatar {
margin-right: -4px; margin-right: -4px;
cursor: pointer; cursor: pointer;
} }
.mx_EventListSummary_toggle { .mx_GenericEventListSummary_toggle {
color: $accent; color: $accent;
cursor: pointer; cursor: pointer;
float: right; float: right;
@ -43,29 +43,29 @@ limitations under the License.
margin-top: 8px; margin-top: 8px;
} }
.mx_EventListSummary_line { .mx_GenericEventListSummary_line {
border-bottom: 1px solid $primary-hairline-color; border-bottom: 1px solid $primary-hairline-color;
margin-left: 63px; margin-left: 63px;
line-height: $font-30px; line-height: $font-30px;
} }
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {
.mx_EventListSummary { .mx_GenericEventListSummary {
font-size: $font-13px; font-size: $font-13px;
.mx_EventTile_line { .mx_EventTile_line {
line-height: $font-20px; line-height: $font-20px;
} }
} }
.mx_EventListSummary_line { .mx_GenericEventListSummary_line {
line-height: $font-22px; line-height: $font-22px;
} }
.mx_EventListSummary_toggle { .mx_GenericEventListSummary_toggle {
margin-top: 3px; margin-top: 3px;
} }
.mx_TextualEvent.mx_EventListSummary_summary { .mx_TextualEvent.mx_GenericEventListSummary_summary {
font-size: $font-13px; font-size: $font-13px;
} }
} }

View file

@ -118,7 +118,7 @@ limitations under the License.
padding-right: 0; padding-right: 0;
} }
.mx_EventTile, .mx_EventListSummary { .mx_EventTile, .mx_GenericEventListSummary {
// Account for scrollbar when hovering // Account for scrollbar when hovering
padding-top: 0; padding-top: 0;

View file

@ -23,7 +23,7 @@ limitations under the License.
} }
.mx_EventTile[data-layout=bubble], .mx_EventTile[data-layout=bubble],
.mx_EventListSummary[data-layout=bubble] { .mx_GenericEventListSummary[data-layout=bubble] {
--avatarSize: 32px; --avatarSize: 32px;
--gutterSize: 11px; --gutterSize: 11px;
--cornerRadius: 12px; --cornerRadius: 12px;
@ -477,7 +477,7 @@ limitations under the License.
.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble], .mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble],
.mx_EventTile.mx_EventTile_leftAlignedBubble[data-layout=bubble], .mx_EventTile.mx_EventTile_leftAlignedBubble[data-layout=bubble],
.mx_EventTile.mx_EventTile_info[data-layout=bubble], .mx_EventTile.mx_EventTile_info[data-layout=bubble],
.mx_EventListSummary[data-layout=bubble][data-expanded=false] { .mx_GenericEventListSummary[data-layout=bubble][data-expanded=false] {
--backgroundColor: transparent; --backgroundColor: transparent;
--gutterSize: 0; --gutterSize: 0;
@ -518,11 +518,11 @@ limitations under the License.
} }
} }
.mx_EventListSummary[data-layout=bubble] { .mx_GenericEventListSummary[data-layout=bubble] {
--maxWidth: 70%; --maxWidth: 70%;
margin-left: calc(var(--avatarSize) + var(--gutterSize)); margin-left: calc(var(--avatarSize) + var(--gutterSize));
.mx_EventListSummary_toggle { .mx_GenericEventListSummary_toggle {
margin: 0 55px 0 5px; margin: 0 55px 0 5px;
float: none; float: none;
@ -534,11 +534,11 @@ limitations under the License.
} }
} }
.mx_EventListSummary_line { .mx_GenericEventListSummary_line {
display: none; display: none;
} }
.mx_EventListSummary_avatars { .mx_GenericEventListSummary_avatars {
padding-top: 0; padding-top: 0;
} }
@ -548,14 +548,14 @@ limitations under the License.
} }
} }
.mx_EventListSummary[data-expanded=false][data-layout=bubble] { .mx_GenericEventListSummary[data-expanded=false][data-layout=bubble] {
// Align with left edge of bubble tiles // Align with left edge of bubble tiles
padding: 0 49px; padding: 0 49px;
} }
// ideally we'd use display=contents here for the layout to all work regardless of the *ELS but // ideally we'd use display=contents here for the layout to all work regardless of the *ELS but
// that breaks ScrollPanel's reliance upon offsetTop so we have to have a bit more finesse. // that breaks ScrollPanel's reliance upon offsetTop so we have to have a bit more finesse.
.mx_EventListSummary[data-expanded=true][data-layout=bubble] { .mx_GenericEventListSummary[data-expanded=true][data-layout=bubble] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0; margin: 0;
@ -579,12 +579,12 @@ limitations under the License.
} }
// increase margin between ELS and the next Event to not have our user avatar overlap the expand/collapse button // increase margin between ELS and the next Event to not have our user avatar overlap the expand/collapse button
.mx_EventListSummary[data-layout=bubble][data-expanded=false] + .mx_EventTile[data-layout=bubble][data-self=true] { .mx_GenericEventListSummary[data-layout=bubble][data-expanded=false] + .mx_EventTile[data-layout=bubble][data-self=true] {
margin-top: 20px; margin-top: 20px;
} }
/* events that do not require bubble layout */ /* events that do not require bubble layout */
.mx_EventListSummary[data-layout=bubble], .mx_GenericEventListSummary[data-layout=bubble],
.mx_EventTile.mx_EventTile_bad[data-layout=bubble] { .mx_EventTile.mx_EventTile_bad[data-layout=bubble] {
.mx_EventTile_line { .mx_EventTile_line {
background: transparent; background: transparent;

View file

@ -281,11 +281,11 @@ $left-gutter: 64px;
} }
.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line, .mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line,
.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line { .mx_GenericEventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line {
padding-left: calc($left-gutter + 18px); padding-left: calc($left-gutter + 18px);
} }
.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line { .mx_GenericEventListSummary:not([data-layout=bubble]) .mx_EventTile_line {
padding-left: $left-gutter; padding-left: $left-gutter;
} }

View file

@ -139,12 +139,12 @@ $irc-line-height: $font-18px;
margin: 0; margin: 0;
} }
.mx_EventListSummary { .mx_GenericEventListSummary {
> .mx_EventTile_line { > .mx_EventTile_line {
padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding
} }
.mx_EventListSummary_avatars { .mx_GenericEventListSummary_avatars {
padding: 0; padding: 0;
margin: 0 9px 0 0; margin: 0 9px 0 0;
} }

View file

@ -20,7 +20,6 @@ import { Room } from 'matrix-js-sdk/src/models/room';
import { EventType } from 'matrix-js-sdk/src/@types/event'; import { EventType } from 'matrix-js-sdk/src/@types/event';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Relations } from "matrix-js-sdk/src/models/relations"; import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import { logger } from 'matrix-js-sdk/src/logger'; import { logger } from 'matrix-js-sdk/src/logger';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
@ -41,8 +40,8 @@ import defaultDispatcher from '../../dispatcher/dispatcher';
import CallEventGrouper from "./CallEventGrouper"; import CallEventGrouper from "./CallEventGrouper";
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
import ScrollPanel, { IScrollState } from "./ScrollPanel"; import ScrollPanel, { IScrollState } from "./ScrollPanel";
import GenericEventListSummary from '../views/elements/GenericEventListSummary';
import EventListSummary from '../views/elements/EventListSummary'; import EventListSummary from '../views/elements/EventListSummary';
import MemberEventListSummary from '../views/elements/MemberEventListSummary';
import DateSeparator from '../views/messages/DateSeparator'; import DateSeparator from '../views/messages/DateSeparator';
import ErrorBoundary from '../views/elements/ErrorBoundary'; import ErrorBoundary from '../views/elements/ErrorBoundary';
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
@ -651,7 +650,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
if (grouper) { if (grouper) {
if (grouper.shouldGroup(mxEv)) { if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv, this.showHiddenEvents); grouper.add(mxEv);
continue; continue;
} else { } else {
// not part of group, so get the group tiles, close the // not part of group, so get the group tiles, close the
@ -1047,7 +1046,7 @@ abstract class BaseGrouper {
} }
public abstract shouldGroup(ev: MatrixEvent): boolean; public abstract shouldGroup(ev: MatrixEvent): boolean;
public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void; public abstract add(ev: MatrixEvent): void;
public abstract getTiles(): ReactNode[]; public abstract getTiles(): ReactNode[];
public abstract getNewPrevEvent(): MatrixEvent; public abstract getNewPrevEvent(): MatrixEvent;
} }
@ -1064,7 +1063,7 @@ abstract class BaseGrouper {
* when determining things such as whether a date separator is necessary * when determining things such as whether a date separator is necessary
*/ */
// Wrap initial room creation events into an EventListSummary // Wrap initial room creation events into a GenericEventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until // Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event // the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
class CreationGrouper extends BaseGrouper { class CreationGrouper extends BaseGrouper {
@ -1140,7 +1139,7 @@ class CreationGrouper extends BaseGrouper {
const eventTiles = this.events.map((e) => { const eventTiles = this.events.map((e) => {
// In order to prevent DateSeparators from appearing in the expanded form // In order to prevent DateSeparators from appearing in the expanded form
// of EventListSummary, render each member event as if the previous // of GenericEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the // one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted. // timestamp of the current event, and no DateSeparator is inserted.
return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
@ -1160,7 +1159,7 @@ class CreationGrouper extends BaseGrouper {
ret.push(<NewRoomIntro key="newroomintro" />); ret.push(<NewRoomIntro key="newroomintro" />);
ret.push( ret.push(
<EventListSummary <GenericEventListSummary
key="roomcreationsummary" key="roomcreationsummary"
events={this.events} events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state onToggle={panel.onHeightChanged} // Update scroll state
@ -1169,7 +1168,7 @@ class CreationGrouper extends BaseGrouper {
layout={this.layout} layout={this.layout}
> >
{ eventTiles } { eventTiles }
</EventListSummary>, </GenericEventListSummary>,
); );
if (this.readMarker) { if (this.readMarker) {
@ -1184,107 +1183,24 @@ class CreationGrouper extends BaseGrouper {
} }
} }
class RedactionGrouper extends BaseGrouper { // Wrap consecutive grouped events in a ListSummary
class MainGrouper extends BaseGrouper {
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
return panel.shouldShowEvent(ev) && ev.isRedacted(); if (!panel.shouldShowEvent(ev)) return false;
};
constructor( if (groupedEvents.includes(ev.getType() as EventType)) {
panel: MessagePanel,
ev: MatrixEvent,
prevEvent: MatrixEvent,
lastShownEvent: MatrixEvent,
layout: Layout,
nextEvent: MatrixEvent,
nextEventTile: MatrixEvent,
) {
super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
this.events = [ev];
}
public shouldGroup(ev: MatrixEvent): boolean {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
if (!this.panel.shouldShowEvent(ev)) {
return true; return true;
} }
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
public add(ev: MatrixEvent): void { if (ev.isRedacted()) {
this.readMarker = this.readMarker || this.panel.readMarkerForEvent( return true;
ev.getId(),
ev === this.lastShownEvent,
);
if (!this.panel.shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
public getTiles(): ReactNode[] {
if (!this.events || !this.events.length) return [];
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
);
} }
const key = "redactioneventlistsummary-" + ( if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) {
this.prevEvent ? this.events[0].getId() : "initial" return true;
);
const senders = new Set<RoomMember>();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
return panel.getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
} }
ret.push( return false;
<EventListSummary
key={key}
threshold={2}
events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
layout={this.layout}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
public getNewPrevEvent(): MatrixEvent {
return this.events[this.events.length - 1];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper extends BaseGrouper {
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
}; };
constructor( constructor(
@ -1293,27 +1209,43 @@ class MemberGrouper extends BaseGrouper {
public readonly prevEvent: MatrixEvent, public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout, protected readonly layout: Layout,
nextEvent: MatrixEvent,
nextEventTile: MatrixEvent,
) { ) {
super(panel, event, prevEvent, lastShownEvent, layout); super(panel, event, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
this.events = [event]; this.events = [event];
} }
public shouldGroup(ev: MatrixEvent): boolean { public shouldGroup(ev: MatrixEvent): boolean {
if (!this.panel.shouldShowEvent(ev)) {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
return true;
}
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false; return false;
} }
return groupedEvents.includes(ev.getType() as EventType); if (groupedEvents.includes(ev.getType() as EventType)) {
return true;
}
if (ev.isRedacted()) {
return true;
}
if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) {
return true;
}
return false;
} }
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void { public add(ev: MatrixEvent): void {
if (ev.getType() === EventType.RoomMember) { if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display // We can ignore any events that don't actually have a message to display
if (!hasText(ev, showHiddenEvents)) return; if (!hasText(ev, this.panel.showHiddenEvents)) return;
}
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent);
if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) {
// absorb hidden events to not split the summary
return;
} }
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev); this.events.push(ev);
} }
@ -1321,7 +1253,7 @@ class MemberGrouper extends BaseGrouper {
// If we don't have any events to group, don't even try to group them. The logic // If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if // below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted. // the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return []; if (!this.events?.length) return [];
const isGrouped = true; const isGrouped = true;
const panel = this.panel; const panel = this.panel;
@ -1335,28 +1267,26 @@ class MemberGrouper extends BaseGrouper {
); );
} }
// Ensure that the key of the MemberEventListSummary does not change with new // Ensure that the key of the EventListSummary does not change with new events.
// member events. This will prevent it from being re-created unnecessarily, and // This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate // instead will allow new props to be provided. In turn, the shouldComponentUpdate
// method on MELS can be used to prevent unnecessary renderings. // method on ELS can be used to prevent unnecessary renderings.
// //
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null, // Whilst back-paginating with an ELS at the top of the panel, prevEvent will be null,
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first // so use the key "eventlistsummary-initial". Otherwise, use the ID of the first
// membership event, which will not change during forward pagination. // membership event, which will not change during forward pagination.
const key = "membereventlistsummary-" + ( const key = "eventlistsummary-" + (this.prevEvent ? this.events[0].getId() : "initial");
this.prevEvent ? this.events[0].getId() : "initial"
);
let highlightInMels; let highlightInSummary = false;
let eventTiles = this.events.map((e) => { let eventTiles = this.events.map((e) => {
if (e.getId() === panel.props.highlightedEventId) { if (e.getId() === panel.props.highlightedEventId) {
highlightInMels = true; highlightInSummary = true;
} }
// In order to prevent DateSeparators from appearing in the expanded form // In order to prevent DateSeparators from appearing in the expanded form
// of MemberEventListSummary, render each member event as if the previous // of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the // one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted. // timestamp of the current event, and no DateSeparator is inserted.
return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []); }).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) { if (eventTiles.length === 0) {
@ -1370,15 +1300,15 @@ class MemberGrouper extends BaseGrouper {
} }
ret.push( ret.push(
<MemberEventListSummary <EventListSummary
key={key} key={key}
events={this.events} events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state onToggle={panel.onHeightChanged} // Update scroll state
startExpanded={highlightInMels} startExpanded={highlightInSummary}
layout={this.layout} layout={this.layout}
> >
{ eventTiles } { eventTiles }
</MemberEventListSummary>, </EventListSummary>,
); );
if (this.readMarker) { if (this.readMarker) {
@ -1393,91 +1323,5 @@ class MemberGrouper extends BaseGrouper {
} }
} }
// Wrap consecutive hidden events in a ListSummary, ignore if redacted
class HiddenEventGrouper extends BaseGrouper {
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
return !panel.shouldShowEvent(ev, true) && panel.showHiddenEvents;
};
constructor(
public readonly panel: MessagePanel,
public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent,
protected readonly layout: Layout,
) {
super(panel, event, prevEvent, lastShownEvent, layout);
this.events = [event];
}
public shouldGroup(ev: MatrixEvent): boolean {
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return !this.panel.shouldShowEvent(ev, true);
}
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent);
this.events.push(ev);
}
public getTiles(): ReactNode[] {
if (!this.events || !this.events.length) return [];
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
<li key={ts+'~'}><DateSeparator key={ts+'~'} roomId={this.events[0].getRoomId()} ts={ts} /></li>,
);
}
const key = "hiddeneventlistsummary-" + (
this.prevEvent ? this.events[0].getId() : "initial"
);
const senders = new Set<RoomMember>();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
return panel.getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
eventTiles = null;
}
ret.push(
<EventListSummary
key={key}
threshold={2}
events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s hidden messages.", { count: eventTiles.length })}
layout={this.layout}
>
{ eventTiles }
</EventListSummary>,
);
if (this.readMarker) {
ret.push(this.readMarker);
}
return ret;
}
public getNewPrevEvent(): MatrixEvent {
return this.events[this.events.length - 1];
}
}
// all the grouper classes that we use // all the grouper classes that we use
const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper, HiddenEventGrouper]; const groupers = [CreationGrouper, MainGrouper];

View file

@ -1,5 +1,7 @@
/* /*
Copyright 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,110 +16,510 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactNode, useEffect } from "react"; import React, { ComponentProps } from 'react';
import { uniqBy } from "lodash";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger"; import { EventType } from 'matrix-js-sdk/src/@types/event';
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle"; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import AccessibleButton from "./AccessibleButton"; import { isValid3pidInvite } from "../../../RoomInvite";
import GenericEventListSummary from "./GenericEventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { jsxJoin } from '../../../utils/ReactUtils';
import { Layout } from '../../../settings/enums/Layout'; import { Layout } from '../../../settings/enums/Layout';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import AccessibleButton from './AccessibleButton';
interface IProps { const onPinnedMessagesClick = (): void => {
// An array of member events to summarise RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
events: MatrixEvent[]; };
// The minimum number of events needed to trigger summarisation
threshold?: number; const TARGET_AS_DISPLAY_NAME_EVENTS = [EventType.RoomMember];
// Whether or not to begin with state.expanded=true
startExpanded?: boolean; interface IProps extends Omit<ComponentProps<typeof GenericEventListSummary>, "summaryText" | "summaryMembers"> {
// The list of room members for which to show avatars next to the summary // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryMembers?: RoomMember[]; summaryLength?: number;
// The text to show as the summary of this event list // The maximum number of avatars to display in the summary
summaryText?: string | JSX.Element; avatarsMaxLength?: number;
// An array of EventTiles to render when expanded // The currently selected layout
children: ReactNode[]; layout: Layout;
// Called when the event list expansion is toggled
onToggle?(): void;
// The layout currently used
layout?: Layout;
} }
const EventListSummary: React.FC<IProps> = ({ interface IUserEvents {
events, // The original event
children, mxEvent: MatrixEvent;
threshold = 3, // The display name of the user (if not, then user ID)
onToggle, displayName: string;
startExpanded, // The original index of the event in this.props.events
summaryMembers = [], index: number;
summaryText, }
layout,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
// Whenever expanded changes call onToggle enum TransitionType {
useEffect(() => { Joined = "joined",
if (onToggle) { Left = "left",
onToggle(); JoinedAndLeft = "joined_and_left",
} LeftAndJoined = "left_and_joined",
}, [expanded]); // eslint-disable-line react-hooks/exhaustive-deps InviteReject = "invite_reject",
InviteWithdrawal = "invite_withdrawal",
Invited = "invited",
Banned = "banned",
Unbanned = "unbanned",
Kicked = "kicked",
ChangedName = "changed_name",
ChangedAvatar = "changed_avatar",
NoChange = "no_change",
ServerAcl = "server_acl",
ChangedPins = "pinned_messages",
MessageRemoved = "message_removed",
HiddenEvent = "hidden_event",
}
const eventIds = events.map((e) => e.getId()).join(','); const SEP = ",";
// If we are only given few events then just pass them through @replaceableComponent("views.elements.EventListSummary")
if (events.length < threshold) { export default class EventListSummary extends React.Component<IProps> {
static defaultProps = {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
layout: Layout.Group,
};
shouldComponentUpdate(nextProps: IProps): boolean {
// Update if
// - The number of summarised events has changed
// - 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 ( return (
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}> nextProps.events.length !== this.props.events.length ||
{ children } nextProps.events.length < this.props.threshold ||
</li> nextProps.layout !== this.props.layout
); );
} }
let body; /**
if (expanded) { * Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
body = <React.Fragment> * the sequences are ordered by `orderedTransitionSequences`.
<div className="mx_EventListSummary_line">&nbsp;</div> * @param {object} eventAggregates a map of transition sequence to array of user display names
{ children } * or user IDs.
</React.Fragment>; * @param {string[]} orderedTransitionSequences an array which is some ordering of
} else { * `Object.keys(eventAggregates)`.
const uniqueMembers = uniqBy(summaryMembers.filter(member => { * @returns {string} the textual summary of the aggregated events that occurred.
if (!member?.getMxcAvatarUrl) { */
logger.error("EventListSummary given null summaryMember, termites may be afoot eating event senders", private generateSummary(
summaryMembers); eventAggregates: Record<string, string[]>,
return false; orderedTransitionSequences: string[],
): string | JSX.Element {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this.renderNameList(userNames);
const splitTransitions = transitions.split(SEP) as TransitionType[];
// Some neighbouring transitions are common, so canonicalise some into "pair"
// transitions
const canonicalTransitions = EventListSummary.getCanonicalTransitions(splitTransitions);
// Transform into consecutive repetitions of the same transition (like 5
// consecutive 'joined_and_left's)
const coalescedTransitions = EventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
const descs = coalescedTransitions.map((t) => {
return EventListSummary.getDescriptionForTransition(
t.transitionType, userNames.length, t.repeats,
);
});
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
});
if (!summaries) {
return null;
}
return jsxJoin(summaries, ", ");
}
/**
* @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".
*/
private renderNameList(users: string[]) {
return formatCommaSeparatedList(users, this.props.summaryLength);
}
/**
* Canonicalise an array of transitions such that some pairs of transitions become
* single transitions. For example an input ['joined','left'] would result in an output
* ['joined_and_left'].
* @param {string[]} transitions an array of transitions.
* @returns {string[]} an array of transitions.
*/
private static getCanonicalTransitions(transitions: TransitionType[]): TransitionType[] {
const modMap = {
[TransitionType.Joined]: {
after: TransitionType.Left,
newTransition: TransitionType.JoinedAndLeft,
},
[TransitionType.Left]: {
after: TransitionType.Joined,
newTransition: TransitionType.LeftAndJoined,
},
// $currentTransition : {
// 'after' : $nextTransition,
// 'newTransition' : 'new_transition_type',
// },
};
const res: TransitionType[] = [];
for (let i = 0; i < transitions.length; i++) {
const t = transitions[i];
const t2 = transitions[i + 1];
let transition = t;
if (i < transitions.length - 1 && modMap[t] && modMap[t].after === t2) {
transition = modMap[t].newTransition;
i++;
} }
return true;
}), member => member.getMxcAvatarUrl()); res.push(transition);
const avatars = uniqueMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />); }
body = ( return res;
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
{ avatars }
</span>
<span className="mx_TextualEvent mx_EventListSummary_summary">
{ summaryText }
</span>
</div>
</div>
);
} }
return ( /**
<li className="mx_EventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""} data-layout={layout}> * Transform an array of transitions into an array of transitions and how many times
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}> * they are repeated consecutively.
{ expanded ? _t('collapse') : _t('expand') } *
</AccessibleButton> * An array of 123 "joined_and_left" transitions, would result in:
{ body } * ```
</li> * [{
); * transitionType: "joined_and_left"
}; * repeats: 123
* }]
* ```
* @param {string[]} transitions the array of transitions to transform.
* @returns {object[]} an array of coalesced transitions.
*/
private static coalesceRepeatedTransitions(transitions: TransitionType[]) {
const res: {
transitionType: TransitionType;
repeats: number;
}[] = [];
EventListSummary.defaultProps = { for (let i = 0; i < transitions.length; i++) {
startExpanded: false, if (res.length > 0 && res[res.length - 1].transitionType === transitions[i]) {
layout: Layout.Group, res[res.length - 1].repeats += 1;
}; } else {
res.push({
transitionType: transitions[i],
repeats: 1,
});
}
}
return res;
}
export default EventListSummary; /**
* For a certain transition, t, describe what happened to the users that
* underwent the transition.
* @param {string} t the transition type.
* @param {number} userCount number of usernames
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition.
*/
private static getDescriptionForTransition(
t: TransitionType,
userCount: number,
count: number,
): string | JSX.Element {
// The empty interpolations 'severalUsers' and 'oneUser'
// are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject.
let res = null;
switch (t) {
case TransitionType.Joined:
res = (userCount > 1)
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count });
break;
case TransitionType.Left:
res = (userCount > 1)
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count });
break;
case TransitionType.JoinedAndLeft:
res = (userCount > 1)
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count });
break;
case TransitionType.LeftAndJoined:
res = (userCount > 1)
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count });
break;
case TransitionType.InviteReject:
res = (userCount > 1)
? _t("%(severalUsers)srejected their invitations %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count });
break;
case TransitionType.InviteWithdrawal:
res = (userCount > 1)
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
severalUsers: "",
count,
})
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count });
break;
case TransitionType.Invited:
res = (userCount > 1)
? _t("were invited %(count)s times", { count })
: _t("was invited %(count)s times", { count });
break;
case TransitionType.Banned:
res = (userCount > 1)
? _t("were banned %(count)s times", { count })
: _t("was banned %(count)s times", { count });
break;
case TransitionType.Unbanned:
res = (userCount > 1)
? _t("were unbanned %(count)s times", { count })
: _t("was unbanned %(count)s times", { count });
break;
case TransitionType.Kicked:
res = (userCount > 1)
? _t("were removed %(count)s times", { count })
: _t("was removed %(count)s times", { count });
break;
case TransitionType.ChangedName:
res = (userCount > 1)
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count });
break;
case TransitionType.ChangedAvatar:
res = (userCount > 1)
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count });
break;
case TransitionType.NoChange:
res = (userCount > 1)
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count });
break;
case TransitionType.ServerAcl:
res = (userCount > 1)
? _t("%(severalUsers)schanged the server ACLs %(count)s times",
{ severalUsers: "", count })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count });
break;
case TransitionType.ChangedPins:
res = (userCount > 1)
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ severalUsers: "", count },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
})
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ oneUser: "", count },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
});
break;
case TransitionType.MessageRemoved:
res = (userCount > 1)
? _t("%(severalUsers)sremoved a message %(count)s times",
{ severalUsers: "", count })
: _t("%(oneUser)sremoved a message %(count)s times", { oneUser: "", count });
break;
case TransitionType.HiddenEvent:
res = (userCount > 1)
? _t("%(severalUsers)ssent %(count)s hidden messages",
{ severalUsers: "", count })
: _t("%(oneUser)ssent %(count)s hidden messages", { oneUser: "", count });
break;
}
return res;
}
private static getTransitionSequence(events: IUserEvents[]) {
return events.map(EventListSummary.getTransition);
}
/**
* Label a given membership event, `e`, where `getContent().membership` has
* changed for each transition allowed by the Matrix protocol. This attempts to
* label the membership changes that occur in `../../../TextForEvent.js`.
* @param {MatrixEvent} e the membership change event to label.
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
private static getTransition(e: IUserEvents): TransitionType {
if (e.mxEvent.isRedacted()) {
return TransitionType.MessageRemoved;
}
switch (e.mxEvent.getType()) {
case EventType.RoomThirdPartyInvite:
// Handle 3pid invites the same as invites so they get bundled together
if (!isValid3pidInvite(e.mxEvent)) {
return TransitionType.InviteWithdrawal;
}
return TransitionType.Invited;
case EventType.RoomServerAcl:
return TransitionType.ServerAcl;
case EventType.RoomPinnedEvents:
return TransitionType.ChangedPins;
case EventType.RoomMember:
switch (e.mxEvent.getContent().membership) {
case 'invite': return TransitionType.Invited;
case 'ban': return TransitionType.Banned;
case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !== e.mxEvent.getPrevContent().displayname) {
return TransitionType.ChangedName;
} else if (e.mxEvent.getContent().avatar_url !== e.mxEvent.getPrevContent().avatar_url) {
return TransitionType.ChangedAvatar;
}
// console.log("MELS ignoring duplicate membership join event");
return TransitionType.NoChange;
} else {
return TransitionType.Joined;
}
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
if (e.mxEvent.getPrevContent().membership === "invite") {
return TransitionType.InviteReject;
}
return TransitionType.Left;
}
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return TransitionType.InviteWithdrawal;
case 'ban': return TransitionType.Unbanned;
// sender is not target and made the target leave, if not from invite/ban then this is a kick
default: return TransitionType.Kicked;
}
default: return null;
}
default:
// otherwise, assume this is a hidden event
return TransitionType.HiddenEvent;
}
}
getAggregate(userEvents: Record<string, IUserEvents[]>) {
// 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.
const aggregate: Record<string, string[]> = {
// $aggregateType : []:string
};
// A map of aggregate types to the indices that order them (the index of
// the first event for a given transition sequence)
const aggregateIndices: Record<string, number> = {
// $aggregateType : int
};
const users = Object.keys(userEvents);
users.forEach(
(userId) => {
const firstEvent = userEvents[userId][0];
const displayName = firstEvent.displayName;
const seq = EventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
aggregate[seq].push(displayName);
if (aggregateIndices[seq] === -1 ||
firstEvent.index < aggregateIndices[seq]
) {
aggregateIndices[seq] = firstEvent.index;
}
},
);
return {
names: aggregate,
indices: aggregateIndices,
};
}
render() {
const eventsToRender = this.props.events;
// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
const latestUserAvatarMember = new Map<string, RoomMember>();
// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const type = e.getType();
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
}
if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
latestUserAvatarMember.set(userId, e.target);
} else {
latestUserAvatarMember.set(userId, e.sender);
}
let displayName = userId;
if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name;
} else {
displayName = e.sender.name;
}
userEvents[userId].push({
mxEvent: e,
displayName,
index: index,
});
});
const aggregate = this.getAggregate(userEvents);
// Sort types by order of lowest event index within sequence
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
(seq1, seq2) => aggregate.indices[seq1] - aggregate.indices[seq2],
);
return <GenericEventListSummary
events={this.props.events}
threshold={this.props.threshold}
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
}
}

View file

@ -0,0 +1,118 @@
/*
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
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, { ReactNode, useEffect } from "react";
import { uniqBy } from "lodash";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
import { Layout } from '../../../settings/enums/Layout';
interface IProps {
// An array of member events to summarise
events: MatrixEvent[];
// The minimum number of events needed to trigger summarisation
threshold?: number;
// Whether or not to begin with state.expanded=true
startExpanded?: boolean;
// The list of room members for which to show avatars next to the summary
summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
summaryText?: string | JSX.Element;
// An array of EventTiles to render when expanded
children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
// The layout currently used
layout?: Layout;
}
const GenericEventListSummary: React.FC<IProps> = ({
events,
children,
threshold = 3,
onToggle,
startExpanded = false,
summaryMembers = [],
summaryText,
layout = Layout.Group,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
// Whenever expanded changes call onToggle
useEffect(() => {
if (onToggle) {
onToggle();
}
}, [expanded]); // eslint-disable-line react-hooks/exhaustive-deps
const eventIds = events.map((e) => e.getId()).join(',');
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
<li className="mx_GenericEventListSummary" data-scroll-tokens={eventIds} data-expanded={true} data-layout={layout}>
{ children }
</li>
);
}
let body;
if (expanded) {
body = <React.Fragment>
<div className="mx_GenericEventListSummary_line">&nbsp;</div>
{ children }
</React.Fragment>;
} else {
const uniqueMembers = uniqBy(summaryMembers.filter(member => {
if (!member?.getMxcAvatarUrl) {
logger.error("EventListSummary given null summaryMember, termites may be afoot eating event senders",
summaryMembers);
return false;
}
return true;
}), member => member.getMxcAvatarUrl());
const avatars = uniqueMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
body = (
<div className="mx_EventTile_line">
<div className="mx_EventTile_info">
<span className="mx_GenericEventListSummary_avatars" onClick={toggleExpanded}>
{ avatars }
</span>
<span className="mx_TextualEvent mx_GenericEventListSummary_summary">
{ summaryText }
</span>
</div>
</div>
);
}
return (
<li className="mx_GenericEventListSummary" data-scroll-tokens={eventIds} data-expanded={expanded + ""} data-layout={layout}>
<AccessibleButton className="mx_GenericEventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
{ body }
</li>
);
};
export default GenericEventListSummary;

View file

@ -1,503 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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, { ComponentProps } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { jsxJoin } from '../../../utils/ReactUtils';
import { Layout } from '../../../settings/enums/Layout';
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import AccessibleButton from './AccessibleButton';
const onPinnedMessagesClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
};
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
// The currently selected layout
layout: Layout;
}
interface IUserEvents {
// The original event
mxEvent: MatrixEvent;
// The display name of the user (if not, then user ID)
displayName: string;
// The original index of the event in this.props.events
index: number;
}
enum TransitionType {
Joined = "joined",
Left = "left",
JoinedAndLeft = "joined_and_left",
LeftAndJoined = "left_and_joined",
InviteReject = "invite_reject",
InviteWithdrawal = "invite_withdrawal",
Invited = "invited",
Banned = "banned",
Unbanned = "unbanned",
Kicked = "kicked",
ChangedName = "changed_name",
ChangedAvatar = "changed_avatar",
NoChange = "no_change",
ServerAcl = "server_acl",
ChangedPins = "pinned_messages"
}
const SEP = ",";
@replaceableComponent("views.elements.MemberEventListSummary")
export default class MemberEventListSummary extends React.Component<IProps> {
static defaultProps = {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
layout: Layout.Group,
};
shouldComponentUpdate(nextProps: IProps): boolean {
// Update if
// - The number of summarised events has changed
// - 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 ||
nextProps.events.length < this.props.threshold ||
nextProps.layout !== this.props.layout
);
}
/**
* Generate the text 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 {string} the textual summary of the aggregated events that occurred.
*/
private generateSummary(
eventAggregates: Record<string, string[]>,
orderedTransitionSequences: string[],
): string | JSX.Element {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this.renderNameList(userNames);
const splitTransitions = transitions.split(SEP) as TransitionType[];
// Some neighbouring transitions are common, so canonicalise some into "pair"
// transitions
const canonicalTransitions = MemberEventListSummary.getCanonicalTransitions(splitTransitions);
// Transform into consecutive repetitions of the same transition (like 5
// consecutive 'joined_and_left's)
const coalescedTransitions = MemberEventListSummary.coalesceRepeatedTransitions(canonicalTransitions);
const descs = coalescedTransitions.map((t) => {
return MemberEventListSummary.getDescriptionForTransition(
t.transitionType, userNames.length, t.repeats,
);
});
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
});
if (!summaries) {
return null;
}
return jsxJoin(summaries, ", ");
}
/**
* @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".
*/
private renderNameList(users: string[]) {
return formatCommaSeparatedList(users, this.props.summaryLength);
}
/**
* Canonicalise an array of transitions such that some pairs of transitions become
* single transitions. For example an input ['joined','left'] would result in an output
* ['joined_and_left'].
* @param {string[]} transitions an array of transitions.
* @returns {string[]} an array of transitions.
*/
private static getCanonicalTransitions(transitions: TransitionType[]): TransitionType[] {
const modMap = {
[TransitionType.Joined]: {
after: TransitionType.Left,
newTransition: TransitionType.JoinedAndLeft,
},
[TransitionType.Left]: {
after: TransitionType.Joined,
newTransition: TransitionType.LeftAndJoined,
},
// $currentTransition : {
// 'after' : $nextTransition,
// 'newTransition' : 'new_transition_type',
// },
};
const res: TransitionType[] = [];
for (let i = 0; i < transitions.length; i++) {
const t = transitions[i];
const 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 coalesced transitions.
*/
private static coalesceRepeatedTransitions(transitions: TransitionType[]) {
const res: {
transitionType: TransitionType;
repeats: number;
}[] = [];
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 {number} userCount number of usernames
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition.
*/
private static getDescriptionForTransition(
t: TransitionType,
userCount: number,
repeats: number,
): string | JSX.Element {
// The empty interpolations 'severalUsers' and 'oneUser'
// are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject.
let res = null;
switch (t) {
case "joined":
res = (userCount > 1)
? _t("%(severalUsers)sjoined %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)sjoined %(count)s times", { oneUser: "", count: repeats });
break;
case "left":
res = (userCount > 1)
? _t("%(severalUsers)sleft %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)sleft %(count)s times", { oneUser: "", count: repeats });
break;
case "joined_and_left":
res = (userCount > 1)
? _t("%(severalUsers)sjoined and left %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)sjoined and left %(count)s times", { oneUser: "", count: repeats });
break;
case "left_and_joined":
res = (userCount > 1)
? _t("%(severalUsers)sleft and rejoined %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)sleft and rejoined %(count)s times", { oneUser: "", count: repeats });
break;
case "invite_reject":
res = (userCount > 1)
? _t("%(severalUsers)srejected their invitations %(count)s times", {
severalUsers: "",
count: repeats,
})
: _t("%(oneUser)srejected their invitation %(count)s times", { oneUser: "", count: repeats });
break;
case "invite_withdrawal":
res = (userCount > 1)
? _t("%(severalUsers)shad their invitations withdrawn %(count)s times", {
severalUsers: "",
count: repeats,
})
: _t("%(oneUser)shad their invitation withdrawn %(count)s times", { oneUser: "", count: repeats });
break;
case "invited":
res = (userCount > 1)
? _t("were invited %(count)s times", { count: repeats })
: _t("was invited %(count)s times", { count: repeats });
break;
case "banned":
res = (userCount > 1)
? _t("were banned %(count)s times", { count: repeats })
: _t("was banned %(count)s times", { count: repeats });
break;
case "unbanned":
res = (userCount > 1)
? _t("were unbanned %(count)s times", { count: repeats })
: _t("was unbanned %(count)s times", { count: repeats });
break;
case "kicked":
res = (userCount > 1)
? _t("were removed %(count)s times", { count: repeats })
: _t("was removed %(count)s times", { count: repeats });
break;
case "changed_name":
res = (userCount > 1)
? _t("%(severalUsers)schanged their name %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)schanged their name %(count)s times", { oneUser: "", count: repeats });
break;
case "changed_avatar":
res = (userCount > 1)
? _t("%(severalUsers)schanged their avatar %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)schanged their avatar %(count)s times", { oneUser: "", count: repeats });
break;
case "no_change":
res = (userCount > 1)
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count: repeats });
break;
case "server_acl":
res = (userCount > 1)
? _t("%(severalUsers)schanged the server ACLs %(count)s times",
{ severalUsers: "", count: repeats })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
break;
case "pinned_messages":
res = (userCount > 1)
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ severalUsers: "", count: repeats },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
})
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ oneUser: "", count: repeats },
{
"a": (sub) => <AccessibleButton kind='link_inline' onClick={onPinnedMessagesClick}>
{ sub }
</AccessibleButton>,
});
break;
}
return res;
}
private static getTransitionSequence(events: IUserEvents[]) {
return events.map(MemberEventListSummary.getTransition);
}
/**
* Label a given membership event, `e`, where `getContent().membership` has
* changed for each transition allowed by the Matrix protocol. This attempts to
* label the membership changes that occur in `../../../TextForEvent.js`.
* @param {MatrixEvent} e the membership change event to label.
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
private static getTransition(e: IUserEvents): TransitionType {
const type = e.mxEvent.getType();
if (type === EventType.RoomThirdPartyInvite) {
// Handle 3pid invites the same as invites so they get bundled together
if (!isValid3pidInvite(e.mxEvent)) {
return TransitionType.InviteWithdrawal;
}
return TransitionType.Invited;
} else if (type === EventType.RoomServerAcl) {
return TransitionType.ServerAcl;
} else if (type === EventType.RoomPinnedEvents) {
return TransitionType.ChangedPins;
}
switch (e.mxEvent.getContent().membership) {
case 'invite': return TransitionType.Invited;
case 'ban': return TransitionType.Banned;
case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !==
e.mxEvent.getPrevContent().displayname) {
return TransitionType.ChangedName;
} else if (e.mxEvent.getContent().avatar_url !==
e.mxEvent.getPrevContent().avatar_url) {
return TransitionType.ChangedAvatar;
}
// console.log("MELS ignoring duplicate membership join event");
return TransitionType.NoChange;
} else {
return TransitionType.Joined;
}
case 'leave':
if (e.mxEvent.getSender() === e.mxEvent.getStateKey()) {
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return TransitionType.InviteReject;
default: return TransitionType.Left;
}
}
switch (e.mxEvent.getPrevContent().membership) {
case 'invite': return TransitionType.InviteWithdrawal;
case 'ban': return TransitionType.Unbanned;
// sender is not target and made the target leave, if not from invite/ban then this is a kick
default: return TransitionType.Kicked;
}
default: return null;
}
}
getAggregate(userEvents: Record<string, IUserEvents[]>) {
// 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.
const aggregate: Record<string, string[]> = {
// $aggregateType : []:string
};
// A map of aggregate types to the indices that order them (the index of
// the first event for a given transition sequence)
const aggregateIndices: Record<string, number> = {
// $aggregateType : int
};
const users = Object.keys(userEvents);
users.forEach(
(userId) => {
const firstEvent = userEvents[userId][0];
const displayName = firstEvent.displayName;
const seq = MemberEventListSummary.getTransitionSequence(userEvents[userId]).join(SEP);
if (!aggregate[seq]) {
aggregate[seq] = [];
aggregateIndices[seq] = -1;
}
aggregate[seq].push(displayName);
if (aggregateIndices[seq] === -1 ||
firstEvent.index < aggregateIndices[seq]
) {
aggregateIndices[seq] = firstEvent.index;
}
},
);
return {
names: aggregate,
indices: aggregateIndices,
};
}
render() {
const eventsToRender = this.props.events;
// Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created,
// so this works perfectly for us to match event order whilst storing the latest Avatar Member
const latestUserAvatarMember = new Map<string, RoomMember>();
// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const type = e.getType();
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
}
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
latestUserAvatarMember.set(userId, e.sender);
} else if (e.target) {
latestUserAvatarMember.set(userId, e.target);
}
let displayName = userId;
if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.sender.name;
} else if (e.target) {
displayName = e.target.name;
}
userEvents[userId].push({
mxEvent: e,
displayName,
index: index,
});
});
const aggregate = this.getAggregate(userEvents);
// Sort types by order of lowest event index within sequence
const orderedTransitionSequences = Object.keys(aggregate.names).sort(
(seq1, seq2) => aggregate.indices[seq1] - aggregate.indices[seq2],
);
return <EventListSummary
events={this.props.events}
threshold={this.props.threshold}
onToggle={this.props.onToggle}
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
}
}

View file

@ -2226,23 +2226,6 @@
"Backspace": "Backspace", "Backspace": "Backspace",
"Join": "Join", "Join": "Join",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse",
"expand": "expand",
"Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s",
"View all %(count)s members|other": "View all %(count)s members",
"View all %(count)s members|one": "View 1 member",
"%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s members including you, %(commaSeparatedMembers)s",
"%(count)s members including you, %(commaSeparatedMembers)s|zero": "You",
"%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s members including you and %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"Rotate Left": "Rotate Left",
"Rotate Right": "Rotate Right",
"Information": "Information",
"Language Dropdown": "Language Dropdown",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined",
@ -2301,7 +2284,33 @@
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.", "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
"%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.|one": "%(severalUsers)schanged the <a>pinned messages</a> for the room.",
"%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.", "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.|other": "%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
"%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved %(count)s messages",
"%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sremoved a message",
"%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved %(count)s messages",
"%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sremoved a message",
"%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)ssent %(count)s hidden messages",
"%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent %(count)s hidden messages",
"%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent a hidden message",
"%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message",
"Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s",
"Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s",
"View all %(count)s members|other": "View all %(count)s members",
"View all %(count)s members|one": "View 1 member",
"%(count)s members including you, %(commaSeparatedMembers)s|other": "%(count)s members including you, %(commaSeparatedMembers)s",
"%(count)s members including you, %(commaSeparatedMembers)s|zero": "You",
"%(count)s members including you, %(commaSeparatedMembers)s|one": "%(count)s members including you and %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s",
"%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"collapse": "collapse",
"expand": "expand",
"Rotate Left": "Rotate Left",
"Rotate Right": "Rotate Right",
"Information": "Information",
"Language Dropdown": "Language Dropdown",
"Create poll": "Create poll", "Create poll": "Create poll",
"Create Poll": "Create Poll", "Create Poll": "Create Poll",
"Failed to post poll": "Failed to post poll", "Failed to post poll": "Failed to post poll",
@ -3056,10 +3065,6 @@
"Logout": "Logout", "Logout": "Logout",
"%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created this DM.": "%(creator)s created this DM.",
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
"%(count)s messages deleted.|other": "%(count)s messages deleted.",
"%(count)s messages deleted.|one": "%(count)s message deleted.",
"%(count)s hidden messages.|other": "%(count)s hidden messages.",
"%(count)s hidden messages.|one": "%(count)s hidden message.",
"Your Communities": "Your Communities", "Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",

View file

@ -122,8 +122,7 @@ describe('MessagePanel', function() {
return events; return events;
} }
// make a collection of events with some member events that should be collapsed // make a collection of events with some member events that should be collapsed with an EventListSummary
// with a MemberEventListSummary
function mkMelsEvents() { function mkMelsEvents() {
const events = []; const events = [];
const ts0 = Date.now(); const ts0 = Date.now();
@ -304,7 +303,7 @@ describe('MessagePanel', function() {
expect(tiles.length).toEqual(2); expect(tiles.length).toEqual(2);
const summaryTiles = TestUtils.scryRenderedComponentsWithType( const summaryTiles = TestUtils.scryRenderedComponentsWithType(
res, sdk.getComponent('elements.MemberEventListSummary'), res, sdk.getComponent('elements.EventListSummary'),
); );
expect(summaryTiles.length).toEqual(1); expect(summaryTiles.length).toEqual(1);
}); });
@ -341,7 +340,7 @@ describe('MessagePanel', function() {
/>, />,
); );
const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_GenericEventListSummary');
// find the <li> which wraps the read marker // find the <li> which wraps the read marker
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
@ -363,7 +362,7 @@ describe('MessagePanel', function() {
/>, />,
); );
const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_EventListSummary'); const summary = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_GenericEventListSummary');
// find the <li> which wraps the read marker // find the <li> which wraps the read marker
const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container'); const rm = TestUtils.findRenderedDOMComponentWithClass(res, 'mx_RoomView_myReadMarker_container');
@ -450,7 +449,7 @@ describe('MessagePanel', function() {
expect(tiles.at(0).props().mxEvent.getType()).toEqual("m.room.create"); expect(tiles.at(0).props().mxEvent.getType()).toEqual("m.room.create");
expect(tiles.at(1).props().mxEvent.getType()).toEqual("m.room.encryption"); expect(tiles.at(1).props().mxEvent.getType()).toEqual("m.room.encryption");
const summaryTiles = res.find(sdk.getComponent('views.elements.EventListSummary')); const summaryTiles = res.find(sdk.getComponent('views.elements.GenericEventListSummary'));
const summaryTile = summaryTiles.at(0); const summaryTile = summaryTiles.at(0);
const summaryEventTiles = summaryTile.find(sdk.getComponent('views.rooms.EventTile')); const summaryEventTiles = summaryTile.find(sdk.getComponent('views.rooms.EventTile'));

View file

@ -5,12 +5,10 @@ import ShallowRenderer from "react-test-renderer/shallow";
import sdk from '../../../skinned-sdk'; import sdk from '../../../skinned-sdk';
import * as testUtils from '../../../test-utils'; import * as testUtils from '../../../test-utils';
// Give MELS a matrixClient in its child context // Give ELS a matrixClient in its child context
const MemberEventListSummary = testUtils.wrapInMatrixClientContext( const EventListSummary = testUtils.wrapInMatrixClientContext(sdk.getComponent('views.elements.EventListSummary'));
sdk.getComponent('views.elements.MemberEventListSummary'),
);
describe('MemberEventListSummary', function() { describe('EventListSummary', function() {
// Generate dummy event tiles for use in simulating an expanded MELS // Generate dummy event tiles for use in simulating an expanded MELS
const generateTiles = (events) => { const generateTiles = (events) => {
return events.map((e) => { return events.map((e) => {
@ -102,7 +100,7 @@ describe('MemberEventListSummary', function() {
}; };
const renderer = new ShallowRenderer(); const renderer = new ShallowRenderer();
renderer.render(<MemberEventListSummary {...props} />); renderer.render(<EventListSummary {...props} />);
const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper
const result = wrapper.props.children; const result = wrapper.props.children;
@ -125,7 +123,7 @@ describe('MemberEventListSummary', function() {
}; };
const renderer = new ShallowRenderer(); const renderer = new ShallowRenderer();
renderer.render(<MemberEventListSummary {...props} />); renderer.render(<EventListSummary {...props} />);
const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper
const result = wrapper.props.children; const result = wrapper.props.children;
@ -150,10 +148,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -186,10 +184,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -234,10 +232,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -286,10 +284,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -345,10 +343,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -383,10 +381,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -433,10 +431,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -507,10 +505,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -554,10 +552,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -589,10 +587,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -617,10 +615,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -644,10 +642,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;
@ -669,10 +667,10 @@ describe('MemberEventListSummary', function() {
}; };
const instance = ReactTestUtils.renderIntoDocument( const instance = ReactTestUtils.renderIntoDocument(
<MemberEventListSummary {...props} />, <EventListSummary {...props} />,
); );
const summary = ReactTestUtils.findRenderedDOMComponentWithClass( const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
instance, "mx_EventListSummary_summary", instance, "mx_GenericEventListSummary_summary",
); );
const summaryText = summary.textContent; const summaryText = summary.textContent;