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

Group pinned message events with MELS #6349

Merged
11 changes: 8 additions & 3 deletions src/components/structures/MessagePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";

const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
const groupedEvents = [
EventType.RoomMember,
EventType.RoomThirdPartyInvite,
EventType.RoomServerAcl,
EventType.RoomPinnedEvents,
];

// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
Expand Down Expand Up @@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
// 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) && membershipTypes.includes(ev.getType() as EventType);
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
};

constructor(
Expand All @@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return membershipTypes.includes(ev.getType() as EventType);
return groupedEvents.includes(ev.getType() as EventType);
}

public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/EventListSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface IProps {
// 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;
summaryText?: string | JSX.Element;
// An array of EventTiles to render when expanded
children: ReactNode[];
// Called when the event list expansion is toggled
Expand Down
58 changes: 47 additions & 11 deletions src/components/views/elements/MemberEventListSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { Action } from '../../../dispatcher/actions';
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
import { jsxJoin } from '../../../utils/ReactUtils';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { Layout } from '../../../settings/Layout';

const onPinnedMessagesClick = (): void => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.PinnedMessages,
allowClose: 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;
Expand Down Expand Up @@ -60,6 +76,7 @@ enum TransitionType {
ChangedAvatar = "changed_avatar",
NoChange = "no_change",
ServerAcl = "server_acl",
ChangedPins = "pinned_messages"
}

const SEP = ",";
Expand Down Expand Up @@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
* `Object.keys(eventAggregates)`.
* @returns {string} the textual summary of the aggregated events that occurred.
*/
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
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);
Expand Down Expand Up @@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
return null;
}

return summaries.join(", ");
return jsxJoin(summaries, ", ");
}

/**
Expand Down Expand Up @@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
* @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) {
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.
Expand Down Expand Up @@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
{ 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) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
{ oneUser: "", count: repeats },
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
break;
}

return res;
Expand All @@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
* if a transition is not recognised.
*/
private static getTransition(e: IUserEvents): TransitionType {
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
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;
}

if (e.mxEvent.getType() === 'm.room.server_acl') {
} else if (type === EventType.RoomServerAcl) {
return TransitionType.ServerAcl;
} else if (type === EventType.RoomPinnedEvents) {
return TransitionType.ChangedPins;
}

switch (e.mxEvent.getContent().membership) {
Expand Down Expand Up @@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
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.getType() === 'm.room.server_acl') {
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 (e.getType() === 'm.room.third_party_invite') {
if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
} else if (e.getType() === 'm.room.server_acl') {
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.sender.name;
} else if (e.target) {
displayName = e.target.name;
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2077,6 +2077,8 @@
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
"%(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",
"%(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.",
"%(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.",
"Power level": "Power level",
"Custom level": "Custom level",
"QR Code": "QR Code",
Expand Down
7 changes: 4 additions & 3 deletions src/utils/FormattingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
*/

import { _t } from '../languageHandler';
import { jsxJoin } from './ReactUtils';

/**
* formats numbers to fit into ~3 characters, suitable for badge counts
Expand Down Expand Up @@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string {
* @returns {string} a string constructed by joining `items` with a comma
* between each item, but with the last item appended as " and [lastItem]".
*/
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0,
);
Expand All @@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
return items[0];
} else if (remaining > 0) {
items = items.slice(0, itemLimit);
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
} else {
const lastItem = items.pop();
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
}
}
33 changes: 33 additions & 0 deletions src/utils/ReactUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@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 from "react";

/**
* Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <span>hello world</span>
* @param array the array of element to join
* @param joiner the string/JSX.Element to join with
* @returns the joined array
*/
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
const newArray = [];
array.forEach((element, index) => {
newArray.push(element, (index === array.length - 1) ? null : joiner);
});
return (
<span>{ newArray }</span>
);
}