mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge pull request #6349 from SimonBrandner/feature/collapse-pinned-mels/17938
Group pinned message events with MELSpull/21833/head
						commit
						f53451df65
					
				|  | @ -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 }); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
|     ); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston