WIP
This commit is contained in:
13
package.json
13
package.json
@@ -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"
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
37
res/css/views/rooms/_MemberListHeaderView.pcss
Normal file
37
res/css/views/rooms/_MemberListHeaderView.pcss
Normal 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;
|
||||
}
|
||||
}
|
||||
19
res/css/views/rooms/_MemberListView.pcss
Normal file
19
res/css/views/rooms/_MemberListView.pcss
Normal 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%;
|
||||
}
|
||||
|
||||
}
|
||||
52
res/css/views/rooms/_MemberTileView.pcss
Normal file
52
res/css/views/rooms/_MemberTileView.pcss
Normal 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);
|
||||
}
|
||||
}
|
||||
43
res/css/views/rooms/_OverflowTile.pcss
Normal file
43
res/css/views/rooms/_OverflowTile.pcss
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
225
src/components/viewmodels/MemberListViewModel.tsx
Normal file
225
src/components/viewmodels/MemberListViewModel.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
179
src/components/viewmodels/MemberTileViewModel.tsx
Normal file
179
src/components/viewmodels/MemberTileViewModel.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
57
src/components/views/avatars/MemberAvatarView.tsx
Normal file
57
src/components/views/avatars/MemberAvatarView.tsx
Normal 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);
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
109
src/components/views/rooms/E2EIconView.tsx
Normal file
109
src/components/views/rooms/E2EIconView.tsx
Normal 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;
|
||||
120
src/components/views/rooms/MemberListHeaderView.tsx
Normal file
120
src/components/views/rooms/MemberListHeaderView.tsx
Normal 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;
|
||||
78
src/components/views/rooms/MemberListView.tsx
Normal file
78
src/components/views/rooms/MemberListView.tsx
Normal 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;
|
||||
71
src/components/views/rooms/MemberTileView.tsx
Normal file
71
src/components/views/rooms/MemberTileView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
32
src/components/views/rooms/OverflowTileView.tsx
Normal file
32
src/components/views/rooms/OverflowTileView.tsx
Normal 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;
|
||||
46
src/components/views/rooms/PresenceIconView.tsx
Normal file
46
src/components/views/rooms/PresenceIconView.tsx
Normal 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;
|
||||
30
src/models/rooms/AvatarThumbnailData.ts
Normal file
30
src/models/rooms/AvatarThumbnailData.ts
Normal 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();
|
||||
}
|
||||
17
src/models/rooms/PresenceState.ts
Normal file
17
src/models/rooms/PresenceState.ts
Normal 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";
|
||||
31
src/models/rooms/RoomMember.ts
Normal file
31
src/models/rooms/RoomMember.ts
Normal 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;
|
||||
};
|
||||
21
src/models/rooms/ThreePIDInvite.ts
Normal file
21
src/models/rooms/ThreePIDInvite.ts
Normal 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;
|
||||
};
|
||||
88
yarn.lock
88
yarn.lock
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user