You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-04 11:51:45 +03:00 
			
		
		
		
	Merge pull request #6349 from SimonBrandner/feature/collapse-pinned-mels/17938
Group pinned message events with MELS
This commit is contained in:
		@@ -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
 | 
			
		||||
@@ -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(
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -60,6 +76,7 @@ enum TransitionType {
 | 
			
		||||
    ChangedAvatar = "changed_avatar",
 | 
			
		||||
    NoChange = "no_change",
 | 
			
		||||
    ServerAcl = "server_acl",
 | 
			
		||||
    ChangedPins = "pinned_messages"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SEP = ",";
 | 
			
		||||
@@ -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);
 | 
			
		||||
@@ -122,7 +142,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return summaries.join(", ");
 | 
			
		||||
        return jsxJoin(summaries, ", ");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -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) {
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -2086,6 +2086,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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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,
 | 
			
		||||
    );
 | 
			
		||||
@@ -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
									
								
								src/utils/ReactUtils.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/utils/ReactUtils.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user