This commit is contained in:
R Midhun Suresh
2024-12-01 23:15:41 +05:30
parent 28640eec5f
commit 7ba63538f8
28 changed files with 1310 additions and 152 deletions

View File

@@ -85,8 +85,8 @@
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@vector-im/compound-design-tokens": "^2.0.1",
"@vector-im/compound-web": "^7.3.0",
"@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.4.0",
"@vector-im/matrix-wysiwyg": "2.37.13",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
@@ -146,7 +146,9 @@
"temporal-polyfill": "^0.2.5",
"ua-parser-js": "^1.0.2",
"uuid": "^11.0.0",
"what-input": "^5.2.10"
"what-input": "^5.2.10",
"@types/react-virtualized": "^9.21.30",
"react-virtualized": "^9.22.5"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -296,5 +298,6 @@
},
"engines": {
"node": ">=20.0.0"
}
}
},
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}

View File

@@ -281,6 +281,8 @@
@import "./views/rooms/_EditMessageComposer.pcss";
@import "./views/rooms/_EmojiButton.pcss";
@import "./views/rooms/_EntityTile.pcss";
@import "./views/rooms/_MemberTileView.pcss";
@import "./views/rooms/_OverflowTile.pcss";
@import "./views/rooms/_EventBubbleTile.pcss";
@import "./views/rooms/_EventPreview.pcss";
@import "./views/rooms/_EventTile.pcss";
@@ -290,7 +292,8 @@
@import "./views/rooms/_LinkPreviewGroup.pcss";
@import "./views/rooms/_LinkPreviewWidget.pcss";
@import "./views/rooms/_LiveContentSummary.pcss";
@import "./views/rooms/_MemberList.pcss";
@import "./views/rooms/_MemberListView.pcss";
@import "./views/rooms/_MemberListHeaderView.pcss";
@import "./views/rooms/_MessageComposer.pcss";
@import "./views/rooms/_MessageComposerFormatBar.pcss";
@import "./views/rooms/_NewRoomIntro.pcss";

View File

@@ -21,8 +21,28 @@ Please see LICENSE files in the repository root for full details.
}
.mx_DisambiguatedProfile_mxid {
margin-inline-start: 5px;
color: $secondary-content;
font-size: var(--cpd-font-size-body-sm);
margin-inline-start: 5px;
}
}
.mx_MemberTileView .mx_DisambiguatedProfile {
display: flex;
flex-direction: column;
.mx_DisambiguatedProfile_mxid {
margin-inline-start: 0;
font: var(--cpd-font-body-sm-regular);
}
span:not(.mx_DisambiguatedProfile_mxid) {
/**
In a member tile, this span element is a flex child and so
we need the following for text overflow to work.
**/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@@ -1,69 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_MemberList {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
.mx_Spinner {
flex: 1 0 auto;
}
.mx_SearchBox {
margin-bottom: 5px;
}
h2 {
text-transform: uppercase;
color: $h3-color;
font-weight: var(--cpd-font-weight-semibold);
font-size: $font-13px;
padding-left: 3px;
padding-right: 12px;
margin-top: 8px;
margin-bottom: 4px;
}
.mx_AutoHideScrollbar {
flex: 1 1 0;
margin-top: var(--cpd-space-3x);
}
}
.mx_MemberList_chevron {
position: absolute;
right: 35px;
margin-top: -15px;
}
.mx_MemberList_border {
overflow-y: auto;
order: 1;
flex: 1 1 0px;
}
.mx_MemberList_query {
height: 16px;
/* stricter rule to override the one in _common.pcss */
&[type="text"] {
font-size: $font-12px;
}
}
.mx_MemberList_wrapper {
padding: 10px;
}
.mx_MemberList_invite {
margin: 0 var(--cpd-space-2x);
width: calc(100% - var(--cpd-space-4x));
}

View File

@@ -0,0 +1,37 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_MemberListHeaderView {
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-gray-400);
.mx_MemberListHeaderView_container {
margin-top: var(--cpd-space-6x);
width: 100%;
}
.mx_MemberListHeaderView_invite_small {
margin-left: var(--cpd-space-3x);
}
.mx_MemberListHeaderView_invite_large {
width: 288px;
height: 36px;
}
.mx_MemberListHeaderView_label {
padding: var(--cpd-space-6x) 0 var(--cpd-space-2x) var(--cpd-space-4x);
box-sizing: border-box;
width: 100%;
color: var(--cpd-color-text-secondary);
font: var(--cpd-font-body-sm-semibold);
}
.mx_MemberListHeaderView_search {
width: 240px;
}
}

View File

@@ -0,0 +1,19 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_MemberListView {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
.mx_MemberListView_container {
height: 100%;
}
}

View File

@@ -0,0 +1,52 @@
.mx_MemberTileView {
display: flex;
padding: var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-4x);
box-sizing: border-box;
height: 56px;
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-gray-300);
.mx_MemberTileView_left,
.mx_MemberTileView_right {
display: flex;
align-items: center;
gap: 8px
}
.mx_MemberTileView_left {
flex-basis: 209px;
flex-grow: 1;
min-width: 0;
}
.mx_MemberTileView_name {
font: var(--cpd-font-body-md-medium);
font-size: 15px;
min-width: 0;
}
.mx_MemberTileView_user_label {
font: var(--cpd-font-body-sm-regular);
font-size: 13px;
}
.mx_MemberTileView_avatar {
position: relative;
height: 32px;
width: 32px;
}
.mx_E2EIconView {
display: flex;
justify-content: center;
align-items: center;
}
.mx_E2EIconView_warning {
color: var(--cpd-color-icon-critical-primary);
}
.mx_E2EIconView_verified {
color: var(--cpd-color-icon-success-primary);
}
}

View File

@@ -0,0 +1,43 @@
.mx_OverflowTileView {
display: flex;
align-items: center;
color: $primary-content;
cursor: pointer;
}
.mx_OverflowTileView_text {
flex: 1 1 0;
overflow: hidden;
font: var(--cpd-font-body-md-regular);
text-overflow: ellipsis;
white-space: nowrap;
font-style: italic;
}
.mx_OverflowTileView:hover {
padding-right: 30px;
position: relative; /* to keep the chevron aligned */
}
.mx_OverflowTileView:hover::before {
content: "";
position: absolute;
top: calc(50% - 8px); /* center */
right: -8px;
mask: url("@vector-im/compound-design-tokens/icons/chevron-right.svg");
mask-repeat: no-repeat;
mask-position: center;
width: 16px;
height: 16px;
background-color: $header-panel-text-primary-color;
}
.mx_OverflowTileView_icon {
padding-left: 3px;
padding-right: 12px;
padding-top: 4px;
padding-bottom: 4px;
position: relative;
line-height: 0;
}

View File

@@ -14,3 +14,28 @@ Please see LICENSE files in the repository root for full details.
.mx_PresenceLabel_online {
color: var(--cpd-color-text-success-primary);
}
.mx_PresenceIconView {
position: absolute;
top: 24px;
left: 24px;
width: 12px;
height: 12px;
display: flex;
justify-content: center;
align-items: center;
background: var(--cpd-color-bg-canvas-default);
border-radius: 100%;
.mx_PresenceIconView_online {
color: var(--cpd-color-icon-accent-primary);
}
.mx_PresenceIconView_offline, .mx_PresenceIconView_dnd {
color: var(--cpd-color-icon-tertiary);
}
.mx_PresenceIconView_unavailable {
color: var(--cpd-color-icon-quaternary);
}
}

View File

@@ -99,7 +99,7 @@ export default class MainSplit extends React.Component<IProps> {
<Resizable
key={this.props.sizeKey}
defaultSize={this.loadSidePanelSize()}
minWidth={264}
minWidth={320}
maxWidth="50%"
enable={{
top: false,

View File

@@ -17,7 +17,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import MemberList from "../views/rooms/MemberList";
// import MemberList from "../views/rooms/MemberList";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
@@ -34,6 +34,7 @@ import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/
import { Action } from "../../dispatcher/actions";
import { XOR } from "../../@types/common";
import ExtensionsCard from "../views/right_panel/ExtensionsCard";
import MemberListView from "../views/rooms/MemberListView";
interface BaseProps {
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
@@ -160,26 +161,28 @@ export default class RightPanel extends React.Component<Props, IState> {
case RightPanelPhases.RoomMemberList:
if (!!roomId) {
card = (
<MemberList
roomId={roomId}
key={roomId}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
<MemberListView roomId={roomId} onClose={this.onClose} />
// <MemberList
// roomId={roomId}
// key={roomId}
// onClose={this.onClose}
// searchQuery={this.state.searchQuery}
// onSearchQueryChanged={this.onSearchQueryChanged}
// />
);
}
break;
case RightPanelPhases.SpaceMemberList:
if (!!cardState?.spaceId || !!roomId) {
card = (
<MemberList
roomId={cardState?.spaceId ?? roomId!}
key={cardState?.spaceId ?? roomId!}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
<MemberListView roomId={cardState?.spaceId ?? roomId!} onClose={this.onClose} />
// <MemberList
// roomId={cardState?.spaceId ?? roomId!}
// key={cardState?.spaceId ?? roomId!}
// onClose={this.onClose}
// searchQuery={this.state.searchQuery}
// onSearchQueryChanged={this.onSearchQueryChanged}
// />
);
}
break;

View File

@@ -0,0 +1,225 @@
/*
Copyright 2024 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 {
ClientEvent,
EventType,
MatrixEvent,
Room,
RoomEvent,
RoomMemberEvent,
RoomState,
RoomStateEvent,
RoomMember as SDKRoomMember,
User,
UserEvent,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { throttle } from "lodash";
import { RoomMember } from "../../models/rooms/RoomMember";
import { mediaFromMxc } from "../../customisations/Media";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { UIComponent } from "../../settings/UIFeature";
import { PresenceState } from "../../models/rooms/PresenceState";
import { useMatrixClientContext } from "../../contexts/MatrixClientContext";
import { SDKContext } from "../../contexts/SDKContext";
import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import { inviteToRoom } from "../../utils/room/inviteToRoom";
import { canInviteTo } from "../../utils/room/canInviteTo";
function sdkRoomMemberToRoomMember(member: SDKRoomMember): RoomMember {
const displayUserId =
UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
roomId: member.roomId,
}) ?? member.userId;
const mxcAvatarURL = member.getMxcAvatarUrl();
const avatarThumbnailUrl =
(mxcAvatarURL && mediaFromMxc(mxcAvatarURL).getThumbnailOfSourceHttp(10, 10)) ?? undefined;
const user = member.user;
let presenceState: PresenceState | undefined = undefined;
if (user) {
presenceState = (user.presence as PresenceState) || undefined;
}
return {
roomId: member.roomId,
userId: member.userId,
displayUserId: displayUserId,
name: member.name,
rawDisplayName: member.rawDisplayName,
disambiguate: member.disambiguate,
avatarThumbnailUrl: avatarThumbnailUrl,
powerLevel: member.powerLevel,
lastModifiedTime: member.getLastModifiedTime(),
presenceState,
isInvite: member.membership === KnownMembership.Invite,
};
}
export interface MemberListViewState {
members: RoomMember[];
memberCount: number;
search: (searchQuery: string) => void;
isPresenceEnabled: boolean;
shouldShowInvite: boolean;
isLoading: boolean;
canInvite: boolean;
onInviteButtonClick: (ev: ButtonEvent) => void;
}
export function useMemberListViewModel(roomId: string): MemberListViewState {
const cli = useMatrixClientContext();
const room = useMemo(() => cli.getRoom(roomId), [roomId, cli]);
if (!room) {
throw new Error(`Room with id ${roomId} does not exist!`);
}
const sdkContext = useContext(SDKContext);
const [members, setMembers] = useState<RoomMember[]>([]);
const [memberCount, setMemberCount] = useState<number>(0);
const searchQuery = useRef("");
const [isLoading, setIsLoading] = useState<boolean>(true);
const loadMembers = useMemo(
() =>
throttle(
async (): Promise<void> => {
const { joined: joinedSdk, invited: invitedSdk } = await sdkContext.memberListStore.loadMemberList(
roomId,
searchQuery.current,
);
const joined = joinedSdk.map(sdkRoomMemberToRoomMember);
const invited = invitedSdk.map(sdkRoomMemberToRoomMember);
setMembers([...invited, ...joined]);
if (!searchQuery.current) setMemberCount(joined.length);
},
500,
{ leading: true, trailing: true },
),
[roomId, sdkContext.memberListStore],
);
const search = useCallback(
(query: string) => {
searchQuery.current = query;
loadMembers();
},
[loadMembers],
);
const isPresenceEnabled = useMemo(
() => sdkContext.memberListStore.isPresenceEnabled(),
[sdkContext.memberListStore],
);
const getCanUserInviteToThisRoom = useCallback((): boolean => !!room && canInviteTo(room), [room]);
const [canInvite, setCanInvite] = useState<boolean>(getCanUserInviteToThisRoom());
const shouldShowInvite = useMemo(() => {
return room?.getMyMembership() == KnownMembership.Join && shouldShowComponent(UIComponent.InviteUsers);
}, [room]);
const onInviteButtonClick = (ev: ButtonEvent): void => {
PosthogTrackers.trackInteraction("WebRightPanelMemberListInviteButton", ev);
ev.preventDefault();
inviteToRoom(room);
};
useEffect(() => {
const onRoomStateUpdate = (state: RoomState): void => {
if (state.roomId === roomId) loadMembers();
};
const onRoomMemberName = (ev: MatrixEvent, member: SDKRoomMember): void => {
if (member.roomId === roomId) loadMembers();
};
const onRoomStateEvent = (event: MatrixEvent): void => {
if (event.getRoomId() === roomId && event.getType() === EventType.RoomThirdPartyInvite) loadMembers();
const newCanInvite = getCanUserInviteToThisRoom();
setCanInvite(newCanInvite);
};
const onRoom = (room: Room): void => {
if (room.roomId === roomId) loadMembers();
// We listen for room events because when we accept an invite
// we need to wait till the room is fully populated with state
// before refreshing the member list else we get a stale list.
// this.onMemberListUpdated?.(true);
};
const onMyMembership = (room: Room, membership: string, oldMembership?: string): void => {
if (room.roomId !== roomId) return;
if (membership === KnownMembership.Join && oldMembership !== KnownMembership.Join) {
// we just joined the room, load the member list
loadMembers();
}
};
const onUserPresenceChange = (event: MatrixEvent | undefined, user: User): void => {
loadMembers();
};
cli.on(RoomStateEvent.Update, onRoomStateUpdate);
cli.on(RoomMemberEvent.Name, onRoomMemberName);
cli.on(RoomStateEvent.Events, onRoomStateEvent);
cli.on(ClientEvent.Room, onRoom); // invites & joining after peek
cli.on(RoomEvent.MyMembership, onMyMembership);
cli.on(UserEvent.LastPresenceTs, onUserPresenceChange);
cli.on(UserEvent.Presence, onUserPresenceChange);
cli.on(UserEvent.CurrentlyActive, onUserPresenceChange);
// Initial load of the memberlist
(async () => {
await loadMembers();
/**
* isLoading is used to render a spinner on initial call.
* Further calls need not mutate this state since it's perfectly fine to
* show the existing memberlist until the new one loads.
*/
setIsLoading(false);
})();
return () => {
cli.off(RoomStateEvent.Update, onRoomStateUpdate);
cli.off(RoomMemberEvent.Name, onRoomMemberName);
cli.off(RoomStateEvent.Events, onRoomStateEvent);
cli.off(ClientEvent.Room, onRoom); // invites & joining after peek
cli.off(RoomEvent.MyMembership, onMyMembership);
cli.off(UserEvent.LastPresenceTs, onUserPresenceChange);
cli.off(UserEvent.Presence, onUserPresenceChange);
cli.off(UserEvent.CurrentlyActive, onUserPresenceChange);
};
}, [cli, loadMembers, roomId, getCanUserInviteToThisRoom]);
return {
members,
memberCount,
search,
shouldShowInvite,
isPresenceEnabled,
isLoading,
onInviteButtonClick,
canInvite,
};
}

View File

@@ -0,0 +1,179 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 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 { useEffect, useMemo, useState } from "react";
import { RoomStateEvent, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import dis from "../../dispatcher/dispatcher";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { Action } from "../../dispatcher/actions";
import { asyncSome } from "../../utils/arrays";
import { getUserDeviceIds } from "../../utils/crypto/deviceInfo";
import { RoomMember } from "../../models/rooms/RoomMember";
import { E2EState } from "../views/rooms/E2EIcon";
import { _t, _td, TranslationKey } from "../../languageHandler";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
interface IProps {
member: RoomMember;
showPresence?: boolean;
}
export interface MemberTileViewState extends IProps {
e2eStatus?: E2EState;
name: string;
onClick: () => void;
title: string;
userLabel?: string;
}
export enum PowerStatus {
Admin = "admin",
Moderator = "moderator",
}
const PowerLabel: Record<PowerStatus, TranslationKey> = {
[PowerStatus.Admin]: _td("power_level|admin"),
[PowerStatus.Moderator]: _td("power_level|mod"),
};
export default function useMemberTileViewModel(props: IProps): MemberTileViewState {
const [e2eStatus, setE2eStatus] = useState<E2EState | undefined>();
useEffect(() => {
const cli = MatrixClientPeg.safeGet();
const updateE2EStatus = async (): Promise<void> => {
const { userId } = props.member;
const isMe = userId === cli.getUserId();
const userTrust = await cli.getCrypto()?.getUserVerificationStatus(userId);
if (!userTrust?.isCrossSigningVerified()) {
setE2eStatus(userTrust?.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal);
return;
}
const deviceIDs = await getUserDeviceIds(cli, userId);
const anyDeviceUnverified = await asyncSome(deviceIDs, async (deviceId) => {
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
return !deviceTrust || (isMe ? !deviceTrust.crossSigningVerified : !deviceTrust.isVerified());
});
setE2eStatus(anyDeviceUnverified ? E2EState.Warning : E2EState.Verified);
};
const onRoomStateEvents = (ev: MatrixEvent): void => {
if (ev.getType() !== EventType.RoomEncryption) return;
const { roomId } = props.member;
if (ev.getRoomId() !== roomId) return;
// The room is encrypted now.
cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
updateE2EStatus();
};
const onUserTrustStatusChanged = (userId: string, trustStatus: UserVerificationStatus): void => {
if (userId !== props.member.userId) return;
updateE2EStatus();
};
const onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
if (userId !== props.member.userId) return;
updateE2EStatus();
};
const { roomId } = props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
if (isRoomEncrypted) {
cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
cli.on(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
updateE2EStatus();
} else {
// Listen for room to become encrypted
cli.on(RoomStateEvent.Events, onRoomStateEvents);
}
}
return () => {
if (cli) {
cli.removeListener(RoomStateEvent.Events, onRoomStateEvents);
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
cli.removeListener(CryptoEvent.DeviceVerificationChanged, onDeviceVerificationChanged);
}
};
}, [props.member]);
const onClick = (): void => {
dis.dispatch({
action: Action.ViewUser,
member: props.member,
push: true,
});
};
const member = props.member;
const name = props.member.name;
const powerStatusMap = new Map([
[100, PowerStatus.Admin],
[50, PowerStatus.Moderator],
]);
// Find the nearest power level with a badge
let powerLevel = props.member.powerLevel;
for (const [pl] of powerStatusMap) {
if (props.member.powerLevel >= pl) {
powerLevel = pl;
break;
}
}
const title = useMemo(() => {
return _t("member_list|power_label", {
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
roomId: member.roomId,
}),
powerLevelNumber: member.powerLevel,
}).trim();
}, [member.powerLevel, member.roomId, member.userId]);
let userLabel;
const powerStatus = powerStatusMap.get(powerLevel);
if (powerStatus) {
userLabel = _t(PowerLabel[powerStatus]);
}
if (props.member.isInvite) {
userLabel = "(Invited)";
}
return {
title,
member,
name,
onClick,
e2eStatus,
showPresence: props.showPresence,
userLabel,
};
}

View File

@@ -0,0 +1,57 @@
/*
Copyright 2024 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, { forwardRef, Ref } from "react";
import BaseAvatar from "./BaseAvatar";
import { _t } from "../../../languageHandler";
import { RoomMember } from "../../../models/rooms/RoomMember";
import { AvatarThumbnailData, avatarUrl } from "../../../models/rooms/AvatarThumbnailData";
interface Props {
member: RoomMember;
size: string;
resizeMethod?: "crop" | "scale";
}
function MemberAvatarView({ size, resizeMethod = "crop", member }: Props, ref: Ref<HTMLElement>): JSX.Element {
let imageUrl = undefined;
const avatarThumbnailUrl = member.avatarThumbnailUrl;
if (!!avatarThumbnailUrl) {
const data: AvatarThumbnailData = {
src: avatarThumbnailUrl,
width: parseInt(size, 10),
height: parseInt(size, 10),
resizeMethod: resizeMethod,
};
imageUrl = avatarUrl(data);
}
return (
<BaseAvatar
size={size}
name={member.name}
idName={member.userId}
title={member.displayUserId}
url={imageUrl}
altText={_t("common|user_avatar")}
ref={ref}
/>
);
}
export default forwardRef(MemberAvatarView);

View File

@@ -23,8 +23,6 @@ import {
TimelineEvents,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
// eslint-disable-next-line no-restricted-imports
import OverflowHorizontalSvg from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
@@ -42,8 +40,6 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
@@ -60,6 +56,7 @@ import {
} from "../../../accessibility/RovingTabIndex";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import OverflowTileView from "../rooms/OverflowTileView";
const AVATAR_SIZE = 30;
@@ -275,17 +272,9 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount: number, totalCount: number): JSX.Element {
const text = _t("common|and_n_others", { count: overflowCount });
return (
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={<BaseAvatar url={OverflowHorizontalSvg} name="..." size="36px" />}
name={text}
showPresence={false}
onClick={() => setTruncateAt(totalCount)}
/>
);
return <OverflowTileView remaining={overflowCount} onClick={() => setTruncateAt(totalCount)} />;
}
const onKeyDown = (ev: React.KeyboardEvent, state: IState): void => {

View File

@@ -8,15 +8,21 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import UserIdentifier from "../../../customisations/UserIdentifier";
interface DisambiguatedMemberInfo {
userId: string;
roomId: string;
rawDisplayName?: string;
disambiguate: boolean;
}
interface IProps {
member?: RoomMember | null;
member?: DisambiguatedMemberInfo | null;
fallbackName: string;
onClick?(): void;
colored?: boolean;

View File

@@ -73,18 +73,17 @@ const E2EIcon: React.FC<XOR<UserProps, RoomProps>> = ({
className,
);
let style: CSSProperties | undefined;
if (size) {
style = { width: `${size}px`, height: `${size}px` };
}
let e2eTitle: TranslationKey | undefined;
if (isUser) {
e2eTitle = crossSigningUserTitles[status];
} else {
e2eTitle = crossSigningRoomTitles[status];
}
let style: CSSProperties | undefined;
if (size) {
style = { width: `${size}px`, height: `${size}px` };
}
const label = e2eTitle ? _t(e2eTitle) : "";
let content: JSX.Element;

View File

@@ -0,0 +1,109 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentProps, CSSProperties } from "react";
import classNames from "classnames";
import { Tooltip } from "@vector-im/compound-web";
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
import { _t, _td, TranslationKey } from "../../../languageHandler";
import { E2EStatus } from "../../../utils/ShieldUtils";
import { XOR } from "../../../@types/common";
import { E2EState } from "./E2EIcon";
// export enum E2EState {
// Verified = "verified",
// Warning = "warning",
// Normal = "normal",
// }
const crossSigningUserTitles: { [key in E2EState]?: TranslationKey } = {
[E2EState.Warning]: _td("encryption|cross_signing_user_warning"),
[E2EState.Normal]: _td("encryption|cross_signing_user_normal"),
[E2EState.Verified]: _td("encryption|cross_signing_user_verified"),
};
const crossSigningRoomTitles: { [key in E2EState]?: TranslationKey } = {
[E2EState.Warning]: _td("encryption|cross_signing_room_warning"),
[E2EState.Normal]: _td("encryption|cross_signing_room_normal"),
[E2EState.Verified]: _td("encryption|cross_signing_room_verified"),
};
function getIconFromStatus(status: E2EState | E2EStatus): React.JSX.Element | undefined {
switch (status) {
case E2EState.Normal:
case E2EStatus.Normal:
return undefined;
case E2EState.Verified:
case E2EStatus.Verified:
return <VerifiedIcon height="16px" width="16px" className="mx_E2EIconView_verified" />;
case E2EState.Warning:
case E2EStatus.Warning:
return <ErrorIcon height="16px" width="16px" className="mx_E2EIconView_warning" />;
}
}
interface Props {
className?: string;
size?: number;
onClick?: () => void;
tooltipPlacement?: ComponentProps<typeof Tooltip>["placement"];
}
interface UserPropsF extends Props {
isUser: true;
status: E2EState | E2EStatus;
}
interface RoomPropsF extends Props {
isUser?: false;
status: E2EStatus;
}
const E2EIcon: React.FC<XOR<UserPropsF, RoomPropsF>> = ({
isUser,
status,
className,
size,
onClick,
tooltipPlacement,
}) => {
const classes = classNames(
{
mx_E2EIconView: true,
},
className,
);
let style: CSSProperties | undefined;
if (size) {
style = { width: `${size}px`, height: `${size}px` };
}
let e2eTitle: TranslationKey | undefined;
if (isUser) {
e2eTitle = crossSigningUserTitles[status];
} else {
e2eTitle = crossSigningRoomTitles[status];
}
const label = e2eTitle ? _t(e2eTitle) : "";
const icon = getIconFromStatus(status);
if (!icon) return null;
return (
<Tooltip label={label} placement={tooltipPlacement} isTriggerInteractive={!!onClick}>
<div className={classes} style={style}>
{icon}
</div>
</Tooltip>
);
};
export default E2EIcon;

View File

@@ -0,0 +1,120 @@
/*
Copyright 2024 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 { Search, Text, Button, Tooltip, InlineSpinner } from "@vector-im/compound-web";
import React from "react";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Flex } from "../../utils/Flex";
import { MemberListViewState } from "../../viewmodels/MemberListViewModel";
import { _t } from "../../../languageHandler";
interface Props {
vm: MemberListViewState;
}
interface TooltipProps {
canInvite: boolean;
children: React.ReactNode;
}
const OptionalTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
if (canInvite) return children;
// If the user isn't allowed to invite others to this room, wrap with a relevant tooltip.
return <Tooltip label={_t("member_list|invite_button_no_perms_tooltip")}>{children}</Tooltip>;
};
/**
* This should be:
* A loading text with spinner while the memberlist loads.
* Member count of the room when there's nothing in the search field.
* Number of matching members during search or 'No result' if search found nothing.
*/
function getHeaderLabelJSX(vm: MemberListViewState): React.ReactNode {
if (vm.isLoading) {
return (
<Flex align="center" gap="8px">
<InlineSpinner /> Loading...
</Flex>
);
}
const filteredMemberCount = vm.members.length;
if (filteredMemberCount === 0) {
return "No matches";
}
return `${filteredMemberCount} Members`;
}
/**
* The top section of the memberlist contains:
* - Just an invite button if the number of members < 20
* - Search bar + invite button if number of members > 20
* - A header label, see function above.
*/
const MemberListHeaderView: React.FC<Props> = (props: Props) => {
const vm = props.vm;
const memberCount = vm.memberCount;
const contentJSX =
memberCount < 20 ? (
<OptionalTooltip canInvite={vm.canInvite}>
<Button
kind="secondary"
size="sm"
Icon={UserAddIcon}
className="mx_MemberListHeaderView_invite_large"
disabled={!vm.canInvite}
onClick={vm.onInviteButtonClick}
>
Invite
</Button>
</OptionalTooltip>
) : (
<>
<Search
className="mx_MemberListHeaderView_search mx_no_textinput"
name="searchMembers"
placeholder="Filter People..."
onChange={(e) => vm.search((e as React.ChangeEvent<HTMLInputElement>).target.value)}
/>
<OptionalTooltip canInvite={vm.canInvite}>
<Button
className="mx_MemberListHeaderView_invite_small"
kind="primary"
onClick={vm.onInviteButtonClick}
size="sm"
iconOnly={true}
Icon={InviteIcon}
disabled={!vm.canInvite}
/>
</OptionalTooltip>
</>
);
return (
<Flex className="mx_MemberListHeaderView" as="header" align="center" justify="space-between" direction="column">
{!vm.isLoading && (
<Flex justify="center" className="mx_MemberListHeaderView_container">
{contentJSX}
</Flex>
)}
<Text as="div" size="sm" weight="semibold" className="mx_MemberListHeaderView_label">
{getHeaderLabelJSX(vm)}
</Text>
</Flex>
);
};
export default MemberListHeaderView;

View File

@@ -0,0 +1,78 @@
/*
Copyright 2024 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 { Form } from "@vector-im/compound-web";
import React from "react";
import { List, ListRowProps } from "react-virtualized/dist/commonjs/List";
import { AutoSizer } from "react-virtualized";
import { Flex } from "../../utils/Flex";
import { useMemberListViewModel } from "../../viewmodels/MemberListViewModel";
import MemberTileNext from "./MemberTileView";
import MemberListHeaderView from "./MemberListHeaderView";
import BaseCard from "../right_panel/BaseCard";
import { _t } from "../../../languageHandler";
interface IProps {
roomId: string;
onClose: () => void;
}
const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId);
const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => {
const member = vm.members[index];
return (
<div key={key} style={style}>
<MemberTileNext member={member} showPresence={false} />
</div>
);
};
const memberCount = vm.members.length;
return (
<BaseCard
id="memberlist-panel"
className="mx_MemberListView"
ariaLabelledBy="memberlist-panel-tab"
role="tabpanel"
header={_t("common|people")}
// footer={footer}
onClose={props.onClose}
>
<Flex align="stretch" direction="column" className="mx_MemberListView_container">
<Form.Root>
<MemberListHeaderView vm={vm} />
</Form.Root>
<AutoSizer>
{({ height, width }) => (
<List
rowRenderer={rowRenderer}
rowHeight={56}
rowCount={memberCount}
height={height}
width={width}
/>
)}
</AutoSizer>
</Flex>
</BaseCard>
);
};
export default MemberListView;

View File

@@ -0,0 +1,71 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019, 2020 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 from "react";
import DisambiguatedProfile from "../messages/DisambiguatedProfile";
import { RoomMember } from "../../../models/rooms/RoomMember";
import MemberAvatarNext from "../avatars/MemberAvatarView";
import useMemberTileViewModel from "../../viewmodels/MemberTileViewModel";
import E2EIcon from "./E2EIconView";
import AvatarPresenceIconView from "./PresenceIconView";
import AccessibleButton from "../elements/AccessibleButton";
interface IProps {
member: RoomMember;
showPresence?: boolean;
}
export default function MemberTileView(props: IProps): JSX.Element {
const vm = useMemberTileViewModel(props);
const member = vm.member;
const av = <MemberAvatarNext member={member} size="32px" aria-hidden="true" />;
const name = vm.name;
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
const presenceState = member.presenceState;
const presenceJSX = vm.showPresence && presenceState && <AvatarPresenceIconView presenceState={presenceState} />;
let userLabelJSX;
if (vm.userLabel) {
userLabelJSX = <div className="mx_MemberTileView_user_label">{vm.userLabel}</div>;
}
let e2eIcon;
if (vm.e2eStatus) {
e2eIcon = <E2EIcon isUser={true} status={vm.e2eStatus} />;
}
// The wrapping div is required to make the magic mouse listener work, for some reason.
return (
<div>
<AccessibleButton className="mx_MemberTileView" title={vm.title} onClick={vm.onClick}>
<div className="mx_MemberTileView_left">
<div className="mx_MemberTileView_avatar">
{av} {presenceJSX}
</div>
<div className="mx_MemberTileView_name">{nameJSX}</div>
</div>
<div className="mx_MemberTileView_right">
{userLabelJSX}
{e2eIcon}
</div>
</AccessibleButton>
</div>
);
}

View File

@@ -0,0 +1,32 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
// eslint-disable-next-line no-restricted-imports
import OverflowHorizontalSvg from "@vector-im/compound-design-tokens/icons/overflow-horizontal.svg";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
interface Props {
// The number of remaining items
remaining: number;
onClick(): void;
}
const OverflowTileView: React.FC<Props> = ({ remaining, onClick }) => {
return (
<AccessibleButton onClick={onClick} className="mx_OverflowTileView">
<div className="mx_OverflowTileView_icon">
<img src={OverflowHorizontalSvg} height="36px" width="36px" alt="overflow icon" />
</div>
<div className="mx_OverflowTileView_text">{_t("common|and_n_others", { count: remaining })}</div>
</AccessibleButton>
);
};
export default OverflowTileView;

View File

@@ -0,0 +1,46 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-solid-8x8";
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
import DNDIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
import classNames from "classnames";
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
interface Props {
className?: string;
presenceState: string;
}
export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
function getIconForPresenceState(state: string): React.JSX.Element {
switch (state) {
case "online":
return <OnlineOrUnavailableIcon height="8px" width="8px" className="mx_PresenceIconView_online" />;
case "offline":
return <OfflineIcon height="8px" width="8px" className="mx_PresenceIconView_offline" />;
case "unavailable":
case "io.element.unreachable":
return <OnlineOrUnavailableIcon height="8px" width="8px" className="mx_PresenceIconView_unavailable" />;
case BUSY_PRESENCE_NAME.name:
case BUSY_PRESENCE_NAME.altName:
return <DNDIcon height="8px" width="8px" className="mx_PresenceIconView_dnd" />;
default:
throw new Error(`Presence state "${state}" is unknown.`);
}
}
const AvatarPresenceIconView: React.FC<Props> = ({ className, presenceState }) => {
const names = classNames("mx_PresenceIconView", className);
return <div className={names}>{getIconForPresenceState(presenceState)}</div>;
};
export default AvatarPresenceIconView;

View File

@@ -0,0 +1,30 @@
/*
Copyright 2024 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.
*/
export type AvatarThumbnailData = {
src: string;
width: number;
height: number;
resizeMethod: "crop" | "scale";
};
export function avatarUrl(data: AvatarThumbnailData): string {
const url = new URL(data.src);
url.searchParams.set("method", data.resizeMethod);
url.searchParams.set("width", Math.round(data.width).toString());
url.searchParams.set("height", Math.round(data.height).toString());
return url.toString();
}

View File

@@ -0,0 +1,17 @@
/*
Copyright 2024 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.
*/
export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable";

View File

@@ -0,0 +1,31 @@
/*
Copyright 2024 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.
*/
export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable";
export type RoomMember = {
roomId: string;
userId: string;
displayUserId: string;
name: string;
rawDisplayName?: string;
disambiguate: boolean;
avatarThumbnailUrl?: string;
powerLevel: number;
lastModifiedTime: number;
presenceState?: PresenceState;
isInvite: boolean;
};

View File

@@ -0,0 +1,21 @@
/*
Copyright 2024 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.
*/
export type ThreePIDInvite = {
eventId: string;
stateKey: string;
displayName: string;
};

View File

@@ -1089,6 +1089,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.7.2":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.9.tgz#65884fd6dc255a775402cc1d9811082918f4bf00"
integrity sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.25.7", "@babel/template@^7.3.3":
version "7.25.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769"
@@ -3166,6 +3173,14 @@
dependencies:
"@types/react" "*"
"@types/react-virtualized@^9.21.30":
version "9.21.30"
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.30.tgz#ba39821bcb2487512a8a2cdd9fbdb5e6fc87fedb"
integrity sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==
dependencies:
"@types/prop-types" "*"
"@types/react" "*"
"@types/react@*", "@types/react@18.3.3":
version "18.3.3"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f"
@@ -3414,15 +3429,15 @@
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@vector-im/compound-design-tokens@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.0.1.tgz#add14494caab16cdbe98f2bdabe726908739def4"
integrity sha512-4nkPcrPII+sejispn+UkWZYFN7LecN39e4WGBupdceiMq0NJrfXrnVtJ9/6BDLgSqHInb6R/IWQkIbPbzfqRMg==
"@vector-im/compound-design-tokens@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.0.tgz#e919d56c2a35c8e652058b3b5e4ca3a56f6c1a23"
integrity sha512-FeeghYSJ6qCXF1orKXj+NYUahF4hLljZH1GHqYz6TmJqNIm01FEE62JGEUOXLjOGwL2pCIQvHDrzpox0QcW3JQ==
"@vector-im/compound-web@^7.3.0":
version "7.3.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.3.0.tgz#9594113ac50bff4794715104a30a60c52d15517d"
integrity sha512-gDppQUtpk5LvNHUg+Zlv9qzo1iBAag0s3g8Ec0qS5q4zGBKG6ruXXrNUKg1aK8rpbo2hYQsGaHM6dD8NqLoq3Q==
"@vector-im/compound-web@^7.4.0":
version "7.4.0"
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.4.0.tgz#a5af8af6346f8ff6c14c70f5d4eb2eab7357a7cc"
integrity sha512-ZRBUeEGNmj/fTkIRa8zGnyVN7ytowpfOtHChqNm+m/+OTJN3o/lOMuQHDV8jeSEW2YwPJqGvPuG/dRr89IcQkA==
dependencies:
"@floating-ui/react" "^0.26.24"
"@radix-ui/react-context-menu" "^2.2.1"
@@ -4468,6 +4483,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^1.0.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -5258,7 +5278,7 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-helpers@^5.0.1:
dom-helpers@^5.0.1, dom-helpers@^5.1.3:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
@@ -8365,7 +8385,7 @@ matrix-events-sdk@0.0.1:
jwt-decode "^4.0.0"
loglevel "^1.7.1"
matrix-events-sdk "0.0.1"
matrix-widget-api "^1.10.0"
matrix-widget-api "^1.8.2"
oidc-client-ts "^3.0.1"
p-retry "4"
sdp-transform "^2.14.1"
@@ -10088,6 +10108,11 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-redux@^7.2.0:
version "7.2.9"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d"
@@ -10138,6 +10163,18 @@ react-transition-group@^4.4.1:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-virtualized@^9.22.5:
version "9.22.5"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.5.tgz#bfb96fed519de378b50d8c0064b92994b3b91620"
integrity sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==
dependencies:
"@babel/runtime" "^7.7.2"
clsx "^1.0.4"
dom-helpers "^5.1.3"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.4"
react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
@@ -10922,16 +10959,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -11035,14 +11063,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12173,16 +12194,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0, wrap-ansi@^9.0.0, "wrap-ansi@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^6.2.0, wrap-ansi@^7.0.0, wrap-ansi@^8.1.0, wrap-ansi@^9.0.0, "wrap-ansi@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==