WIP
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user