diff --git a/.eslintrc.js b/.eslintrc.js index 444388d492b..94cf8bbc0cf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -225,6 +225,7 @@ module.exports = { "src/components/views/messages/MVoiceMessageBody.tsx", "src/components/views/right_panel/EncryptionPanel.tsx", "src/components/views/rooms/EntityTile.tsx", + "src/components/views/rooms/EntityTileRefactored.tsx", "src/components/views/rooms/LinkPreviewGroup.tsx", "src/components/views/rooms/MemberList.tsx", "src/components/views/rooms/MessageComposer.tsx", diff --git a/package.json b/package.json index a5479d9ec50..3c41ab459a8 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^8.0.0", "@testing-library/react-hooks": "^8.0.1", + "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^1.8.0", "@vector-im/compound-web": "^5.5.0", "@zxcvbn-ts/core": "^3.0.4", @@ -120,6 +121,7 @@ "matrix-widget-api": "^1.8.2", "memoize-one": "^6.0.0", "minimist": "^1.2.5", + "observable-hooks": "^4.2.3", "oidc-client-ts": "^3.0.1", "opus-recorder": "^8.0.3", "pako": "^2.0.3", @@ -133,7 +135,9 @@ "react-dom": "17.0.2", "react-focus-lock": "^2.5.1", "react-transition-group": "^4.4.1", + "react-virtualized": "^9.22.5", "rfc4648": "^1.4.0", + "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", "sanitize-html": "2.13.0", "tar-js": "^0.3.0", diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index d21c435b248..60ac644df40 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -72,3 +72,8 @@ limitations under the License. order: 2; margin: auto; } + +.mx_MemberList_container { + height: 100%; +} + diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 8f1ba7aecfb..52e3cb856ff 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -26,7 +26,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import SettingsStore from "../../settings/SettingsStore"; -import MemberList from "../views/rooms/MemberList"; import UserInfo from "../views/right_panel/UserInfo"; import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; import FilePanel from "./FilePanel"; @@ -42,6 +41,11 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState"; import { Action } from "../../dispatcher/actions"; import { XOR } from "../../@types/common"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import { inviteToRoom } from "../../utils/room/inviteToRoom"; +import MemberList from "../views/rooms/MemberList"; +import { SpaceScopeHeader } from "../views/rooms/SpaceScopeHeader"; +import MemberListNext from "../views/rooms/MemberListNext"; import { RightPanelTabs } from "../views/right_panel/RightPanelTabs"; import ExtensionsCard from "../views/right_panel/ExtensionsCard"; @@ -67,7 +71,6 @@ type Props = XOR; interface IState { phase?: RightPanelPhases; - searchQuery: string; cardState?: IRightPanelCardState; } @@ -78,9 +81,7 @@ export default class RightPanel extends React.Component { public constructor(props: Props, context: React.ContextType) { super(props, context); - this.state = { - searchQuery: "", - }; + this.state = {}; } private readonly delayedUpdate = throttle( @@ -157,8 +158,19 @@ export default class RightPanel extends React.Component { } }; - private onSearchQueryChanged = (searchQuery: string): void => { - this.setState({ searchQuery }); + private onThreePIDInviteClick = (eventId: string): void => { + const inviteEvent = this.props.room?.findEventById(eventId); + if (!inviteEvent) return; + dis.dispatch({ + action: Action.View3pidInvite, + event: inviteEvent, + }); + }; + + private onInviteButtonClick = (roomId: string): void => { + const cli = MatrixClientPeg.safeGet(); + const room = cli.getRoom(roomId)!; + inviteToRoom(room); }; public render(): React.ReactNode { @@ -170,27 +182,33 @@ export default class RightPanel extends React.Component { case RightPanelPhases.RoomMemberList: if (!!roomId) { card = ( - + + // ); } break; case RightPanelPhases.SpaceMemberList: - if (!!cardState?.spaceId || !!roomId) { + const targetRoomId = cardState?.spaceId ?? roomId + if (!!targetRoomId) { + // const cli = MatrixClientPeg.safeGet(); + // const room = cli.getRoom(roomId); + // const spaceHeader = room ? : undefined; card = ( - + + // ); } break; diff --git a/src/components/views/avatars/MemberAvatarNext.tsx b/src/components/views/avatars/MemberAvatarNext.tsx new file mode 100644 index 00000000000..2ddd994c73b --- /dev/null +++ b/src/components/views/avatars/MemberAvatarNext.tsx @@ -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 IProps { + member: RoomMember; + size: string; + resizeMethod?: "crop" | "scale"; +} + +function MemberAvatarNext({ size, resizeMethod = "crop", member }: IProps, ref: Ref): JSX.Element { + let imageUrl = null; + 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 ( + + ); +} + +export default forwardRef(MemberAvatarNext); diff --git a/src/components/views/dialogs/UntrustedDeviceDialog.tsx b/src/components/views/dialogs/UntrustedDeviceDialog.tsx index dadb199a1ea..29c4c4700e1 100644 --- a/src/components/views/dialogs/UntrustedDeviceDialog.tsx +++ b/src/components/views/dialogs/UntrustedDeviceDialog.tsx @@ -19,7 +19,8 @@ import { User } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import E2EIcon, { E2EState } from "../rooms/E2EIcon"; +import E2EIcon from "../rooms/E2EIcon"; +import { E2EState } from "../../../models/rooms/E2EState"; import AccessibleButton from "../elements/AccessibleButton"; import BaseDialog from "./BaseDialog"; import { IDevice } from "../right_panel/UserInfo"; diff --git a/src/components/views/messages/DisambiguatedProfile.tsx b/src/components/views/messages/DisambiguatedProfile.tsx index da461433855..aca870e68f6 100644 --- a/src/components/views/messages/DisambiguatedProfile.tsx +++ b/src/components/views/messages/DisambiguatedProfile.tsx @@ -16,15 +16,21 @@ limitations under the License. */ 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; diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index 5e9a17a8c5f..ff17c7a0b61 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -31,7 +31,8 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import VerificationQRCode from "../elements/crypto/VerificationQRCode"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import E2EIcon, { E2EState } from "../rooms/E2EIcon"; +import E2EIcon from "../rooms/E2EIcon"; +import { E2EState } from "../../../models/rooms/E2EState"; import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import VerificationShowSas from "../verification/VerificationShowSas"; diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index e655d8b86e2..f9d7c37e55f 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -23,14 +23,7 @@ import { _t, _td, TranslationKey } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import { E2EStatus } from "../../../utils/ShieldUtils"; import { XOR } from "../../../@types/common"; - -export enum E2EState { - Verified = "verified", - Warning = "warning", - Unknown = "unknown", - Normal = "normal", - Unauthenticated = "unauthenticated", -} +import { E2EState } from "../../../models/rooms/E2EState"; const crossSigningUserTitles: { [key in E2EState]?: TranslationKey } = { [E2EState.Warning]: _td("encryption|cross_signing_user_warning"), diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx index cfb579b11cb..002abedbf83 100644 --- a/src/components/views/rooms/EntityTile.tsx +++ b/src/components/views/rooms/EntityTile.tsx @@ -21,9 +21,11 @@ import classNames from "classnames"; import AccessibleButton from "../elements/AccessibleButton"; import { _t, _td, TranslationKey } from "../../../languageHandler"; -import E2EIcon, { E2EState } from "./E2EIcon"; +import E2EIcon from "./E2EIcon"; +import { E2EState } from "../../../models/rooms/E2EState"; import BaseAvatar from "../avatars/BaseAvatar"; import PresenceLabel from "./PresenceLabel"; +import { PresenceState } from "../../../models/rooms/PresenceState"; export enum PowerStatus { Admin = "admin", @@ -35,8 +37,6 @@ const PowerLabel: Record = { [PowerStatus.Moderator]: _td("power_level|mod"), }; -export type PresenceState = "offline" | "online" | "unavailable" | "io.element.unreachable"; - const PRESENCE_CLASS: Record = { "offline": "mx_EntityTile_offline", "online": "mx_EntityTile_online", diff --git a/src/components/views/rooms/EntityTileRefactored.tsx b/src/components/views/rooms/EntityTileRefactored.tsx new file mode 100644 index 00000000000..3103c92dd47 --- /dev/null +++ b/src/components/views/rooms/EntityTileRefactored.tsx @@ -0,0 +1,169 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 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, { useCallback } from "react"; +import classNames from "classnames"; + +import AccessibleButton from "../elements/AccessibleButton"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; +import E2EIcon from "./E2EIcon"; +import { E2EState } from "../../../models/rooms/E2EState"; +import BaseAvatar from "../avatars/BaseAvatar"; +import PresenceLabel from "./PresenceLabel"; +import { PresenceState } from "../../../models/rooms/PresenceState"; + +export enum PowerStatus { + Admin = "admin", + Moderator = "moderator", +} + +const PowerLabel: Record = { + [PowerStatus.Admin]: _td("power_level|admin"), + [PowerStatus.Moderator]: _td("power_level|mod"), +}; + +const PRESENCE_CLASS: Record = { + "offline": "mx_EntityTile_offline", + "online": "mx_EntityTile_online", + "unavailable": "mx_EntityTile_unavailable", + "io.element.unreachable": "mx_EntityTile_unreachable", +}; + +function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string { + if (showPresence === false) { + return "mx_EntityTile_online_beenactive"; + } + + // offline is split into two categories depending on whether we have + // a last_active_ago for them. + if (presenceState === "offline") { + if (lastActiveAgo) { + return PRESENCE_CLASS["offline"] + "_beenactive"; + } else { + return PRESENCE_CLASS["offline"] + "_neveractive"; + } + } else if (presenceState) { + return PRESENCE_CLASS[presenceState]; + } else { + return PRESENCE_CLASS["offline"] + "_neveractive"; + } +} + +interface IProps { + name?: string; + nameJSX?: JSX.Element; + title?: string; + avatarJsx?: JSX.Element; // + className?: string; + presenceState?: PresenceState; + presenceLastActiveAgo: number; + presenceLastTs: number; + presenceCurrentlyActive?: boolean; + showInviteButton?: boolean; + onClick(): void; + showPresence?: boolean; + subtextLabel?: string; + e2eStatus?: E2EState; + powerStatus?: PowerStatus; +} + +export default function EntityTileRefactored({ + onClick = () => {}, + presenceState = "offline", + presenceLastActiveAgo = 0, + presenceLastTs = 0, + showInviteButton = false, + showPresence = true, + ...props +}: IProps): JSX.Element { + /** + * Creates the PresenceLabel component if needed + * @returns The PresenceLabel component if we need to render it, undefined otherwise + */ + const getPresenceLabel = useCallback((): JSX.Element | undefined => { + if (!showPresence) return; + const activeAgo = presenceLastActiveAgo ? Date.now() - (presenceLastTs - presenceLastActiveAgo) : -1; + return ( + + ); + }, [presenceLastTs, presenceLastActiveAgo, presenceState, props.presenceCurrentlyActive, showPresence]); + + const mainClassNames: Record = { + mx_EntityTile: true, + }; + if (props.className) mainClassNames[props.className] = true; + + const presenceClass = presenceClassForMember(presenceState, presenceLastActiveAgo, showPresence); + mainClassNames[presenceClass] = true; + + const name = props.nameJSX || props.name; + const nameAndPresence = ( +
+
{name}
+ {getPresenceLabel()} +
+ ); + + let inviteButton; + if (showInviteButton) { + inviteButton = ( +
+ {_t("action|invite")} +
+ ); + } + + let powerLabel; + const powerStatus = props.powerStatus; + if (powerStatus) { + const powerText = _t(PowerLabel[powerStatus]); + powerLabel =
{powerText}
; + } + + let e2eIcon; + const { e2eStatus } = props; + if (e2eStatus) { + e2eIcon = ; + } + + const av = props.avatarJsx ||