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

Commit f53451d

Browse files
authored
Merge pull request #6349 from SimonBrandner/feature/collapse-pinned-mels/17938
Group pinned message events with MELS
2 parents 3153e11 + 5f68ad9 commit f53451d

File tree

6 files changed

+95
-18
lines changed

6 files changed

+95
-18
lines changed

src/components/structures/MessagePanel.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
5151

5252
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
5353
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
54-
const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
54+
const groupedEvents = [
55+
EventType.RoomMember,
56+
EventType.RoomThirdPartyInvite,
57+
EventType.RoomServerAcl,
58+
EventType.RoomPinnedEvents,
59+
];
5560

5661
// check if there is a previous event and it has the same sender as this event
5762
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
@@ -1234,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
12341239
// Wrap consecutive member events in a ListSummary, ignore if redacted
12351240
class MemberGrouper extends BaseGrouper {
12361241
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
1237-
return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
1242+
return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
12381243
};
12391244

12401245
constructor(
@@ -1252,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
12521257
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
12531258
return false;
12541259
}
1255-
return membershipTypes.includes(ev.getType() as EventType);
1260+
return groupedEvents.includes(ev.getType() as EventType);
12561261
}
12571262

12581263
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {

src/components/views/elements/EventListSummary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ interface IProps {
3434
// The list of room members for which to show avatars next to the summary
3535
summaryMembers?: RoomMember[];
3636
// The text to show as the summary of this event list
37-
summaryText?: string;
37+
summaryText?: string | JSX.Element;
3838
// An array of EventTiles to render when expanded
3939
children: ReactNode[];
4040
// Called when the event list expansion is toggled

src/components/views/elements/MemberEventListSummary.tsx

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,24 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
2525
import { isValid3pidInvite } from "../../../RoomInvite";
2626
import EventListSummary from "./EventListSummary";
2727
import { replaceableComponent } from "../../../utils/replaceableComponent";
28+
import defaultDispatcher from '../../../dispatcher/dispatcher';
29+
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
30+
import { Action } from '../../../dispatcher/actions';
31+
import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
32+
import { jsxJoin } from '../../../utils/ReactUtils';
33+
import { EventType } from 'matrix-js-sdk/src/@types/event';
2834
import { Layout } from '../../../settings/Layout';
2935

36+
const onPinnedMessagesClick = (): void => {
37+
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
38+
action: Action.SetRightPanelPhase,
39+
phase: RightPanelPhases.PinnedMessages,
40+
allowClose: false,
41+
});
42+
};
43+
44+
const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
45+
3046
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
3147
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
3248
summaryLength?: number;
@@ -60,6 +76,7 @@ enum TransitionType {
6076
ChangedAvatar = "changed_avatar",
6177
NoChange = "no_change",
6278
ServerAcl = "server_acl",
79+
ChangedPins = "pinned_messages"
6380
}
6481

6582
const SEP = ",";
@@ -93,7 +110,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
93110
* `Object.keys(eventAggregates)`.
94111
* @returns {string} the textual summary of the aggregated events that occurred.
95112
*/
96-
private generateSummary(eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[]) {
113+
private generateSummary(
114+
eventAggregates: Record<string, string[]>,
115+
orderedTransitionSequences: string[],
116+
): string | JSX.Element {
97117
const summaries = orderedTransitionSequences.map((transitions) => {
98118
const userNames = eventAggregates[transitions];
99119
const nameList = this.renderNameList(userNames);
@@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
122142
return null;
123143
}
124144

125-
return summaries.join(", ");
145+
return jsxJoin(summaries, ", ");
126146
}
127147

128148
/**
@@ -216,7 +236,11 @@ export default class MemberEventListSummary extends React.Component<IProps> {
216236
* @param {number} repeats the number of times the transition was repeated in a row.
217237
* @returns {string} the written Human Readable equivalent of the transition.
218238
*/
219-
private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
239+
private static getDescriptionForTransition(
240+
t: TransitionType,
241+
userCount: number,
242+
repeats: number,
243+
): string | JSX.Element {
220244
// The empty interpolations 'severalUsers' and 'oneUser'
221245
// are there only to show translators to non-English languages
222246
// that the verb is conjugated to plural or singular Subject.
@@ -299,6 +323,15 @@ export default class MemberEventListSummary extends React.Component<IProps> {
299323
{ severalUsers: "", count: repeats })
300324
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
301325
break;
326+
case "pinned_messages":
327+
res = (userCount > 1)
328+
? _t("%(severalUsers)schanged the <a>pinned messages</a> for the room %(count)s times.",
329+
{ severalUsers: "", count: repeats },
330+
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> })
331+
: _t("%(oneUser)schanged the <a>pinned messages</a> for the room %(count)s times.",
332+
{ oneUser: "", count: repeats },
333+
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> });
334+
break;
302335
}
303336

304337
return res;
@@ -317,16 +350,18 @@ export default class MemberEventListSummary extends React.Component<IProps> {
317350
* if a transition is not recognised.
318351
*/
319352
private static getTransition(e: IUserEvents): TransitionType {
320-
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
353+
const type = e.mxEvent.getType();
354+
355+
if (type === EventType.RoomThirdPartyInvite) {
321356
// Handle 3pid invites the same as invites so they get bundled together
322357
if (!isValid3pidInvite(e.mxEvent)) {
323358
return TransitionType.InviteWithdrawal;
324359
}
325360
return TransitionType.Invited;
326-
}
327-
328-
if (e.mxEvent.getType() === 'm.room.server_acl') {
361+
} else if (type === EventType.RoomServerAcl) {
329362
return TransitionType.ServerAcl;
363+
} else if (type === EventType.RoomPinnedEvents) {
364+
return TransitionType.ChangedPins;
330365
}
331366

332367
switch (e.mxEvent.getContent().membership) {
@@ -415,22 +450,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
415450
// Object mapping user IDs to an array of IUserEvents
416451
const userEvents: Record<string, IUserEvents[]> = {};
417452
eventsToRender.forEach((e, index) => {
418-
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
453+
const type = e.getType();
454+
const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
419455
// Initialise a user's events
420456
if (!userEvents[userId]) {
421457
userEvents[userId] = [];
422458
}
423459

424-
if (e.getType() === 'm.room.server_acl') {
460+
if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
425461
latestUserAvatarMember.set(userId, e.sender);
426462
} else if (e.target) {
427463
latestUserAvatarMember.set(userId, e.target);
428464
}
429465

430466
let displayName = userId;
431-
if (e.getType() === 'm.room.third_party_invite') {
467+
if (type === EventType.RoomThirdPartyInvite) {
432468
displayName = e.getContent().display_name;
433-
} else if (e.getType() === 'm.room.server_acl') {
469+
} else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
434470
displayName = e.sender.name;
435471
} else if (e.target) {
436472
displayName = e.target.name;

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2086,6 +2086,8 @@
20862086
"%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs",
20872087
"%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times",
20882088
"%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs",
2089+
"%(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.",
2090+
"%(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.",
20892091
"Power level": "Power level",
20902092
"Custom level": "Custom level",
20912093
"QR Code": "QR Code",

src/utils/FormattingUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
*/
1717

1818
import { _t } from '../languageHandler';
19+
import { jsxJoin } from './ReactUtils';
1920

2021
/**
2122
* formats numbers to fit into ~3 characters, suitable for badge counts
@@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string {
103104
* @returns {string} a string constructed by joining `items` with a comma
104105
* between each item, but with the last item appended as " and [lastItem]".
105106
*/
106-
export function formatCommaSeparatedList(items: string[], itemLimit?: number): string {
107+
export function formatCommaSeparatedList(items: Array<string | JSX.Element>, itemLimit?: number): string | JSX.Element {
107108
const remaining = itemLimit === undefined ? 0 : Math.max(
108109
items.length - itemLimit, 0,
109110
);
@@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s
113114
return items[0];
114115
} else if (remaining > 0) {
115116
items = items.slice(0, itemLimit);
116-
return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } );
117+
return _t("%(items)s and %(count)s others", { items: jsxJoin(items, ', '), count: remaining } );
117118
} else {
118119
const lastItem = items.pop();
119-
return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem });
120+
return _t("%(items)s and %(lastItem)s", { items: jsxJoin(items, ', '), lastItem: lastItem });
120121
}
121122
}

src/utils/ReactUtils.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from "react";
18+
19+
/**
20+
* Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> <span>hello world</span>
21+
* @param array the array of element to join
22+
* @param joiner the string/JSX.Element to join with
23+
* @returns the joined array
24+
*/
25+
export function jsxJoin(array: Array<string | JSX.Element>, joiner?: string | JSX.Element): JSX.Element {
26+
const newArray = [];
27+
array.forEach((element, index) => {
28+
newArray.push(element, (index === array.length - 1) ? null : joiner);
29+
});
30+
return (
31+
<span>{ newArray }</span>
32+
);
33+
}

0 commit comments

Comments
 (0)