MVVM userinfo basic component (#30305)
* feat: mvvm userinfo basic component * test: mvvm userinfobasic component * chore: apply review. rename views, add comment and move some codes * chore(review): move openDM method into viewmodel
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useContext } from "react";
|
||||
import { RoomMember, User, type Room, KnownMembership } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Modal from "../../../../Modal";
|
||||
import ErrorDialog from "../../../views/dialogs/ErrorDialog";
|
||||
import { _t, UserFriendlyError } from "../../../../languageHandler";
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import dis from "../../../../dispatcher/dispatcher";
|
||||
import PosthogTrackers from "../../../../PosthogTrackers";
|
||||
import { ShareDialog } from "../../../views/dialogs/ShareDialog";
|
||||
import { type ComposerInsertPayload } from "../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import { SdkContextClass } from "../../../../contexts/SDKContext";
|
||||
import { TimelineRenderingType } from "../../../../contexts/RoomContext";
|
||||
import MultiInviter from "../../../../utils/MultiInviter";
|
||||
import { type ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { useRoomPermissions } from "./UserInfoBasicViewModel";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../../utils/direct-messages";
|
||||
import { type Member } from "../../../views/right_panel/UserInfo";
|
||||
|
||||
export interface UserInfoBasicOptionsState {
|
||||
// boolean to know if selected user is current user
|
||||
isMe: boolean;
|
||||
// boolean to display/hide invite button
|
||||
showInviteButton: boolean;
|
||||
// boolean to display/hide insert pill button
|
||||
showInsertPillButton: boolean | "";
|
||||
// boolean to display/hide read receipt button
|
||||
readReceiptButtonDisabled: boolean;
|
||||
// Method called when a insert pill button is clicked
|
||||
onInsertPillButton: () => void;
|
||||
// Method called when a read receipt button is clicked, will add a pill in the input message field
|
||||
onReadReceiptButton: () => void;
|
||||
// Method called when a share user button is clicked, will display modal with profile to share
|
||||
onShareUserClick: () => void;
|
||||
// Method called when a invite button is clicked, will display modal to invite user
|
||||
onInviteUserButton: (evt: Event) => Promise<void>;
|
||||
// Method called when the DM button is clicked, will open a DM with the selected member
|
||||
onOpenDmForUser: (member: Member) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useUserInfoBasicOptionsViewModel = (room: Room, member: User | RoomMember): UserInfoBasicOptionsState => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
// selected member is current user
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
// Those permissions are updated when a change is done on the room current state and the selected user
|
||||
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
|
||||
// read receipt button stay disable for a room space or if all events where read (null)
|
||||
const readReceiptButtonDisabled = isSpace || !room?.getEventReadUpTo(member.userId);
|
||||
|
||||
// always show exempt when room is a space
|
||||
const showInsertPillButton = member instanceof RoomMember && member.roomId && !isSpace;
|
||||
|
||||
// show invite button only if current user has the permission to invite and the selected user membership is LEAVE
|
||||
const showInviteButton =
|
||||
member instanceof RoomMember &&
|
||||
roomPermissions.canInvite &&
|
||||
(member?.membership ?? KnownMembership.Leave) === KnownMembership.Leave;
|
||||
|
||||
const onReadReceiptButton = function (): void {
|
||||
const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : null;
|
||||
if (!room || readReceiptButtonDisabled) return;
|
||||
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
highlighted: true,
|
||||
// this could return null, the default prevents a type error
|
||||
event_id: room.getEventReadUpTo(member.userId) || undefined,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
};
|
||||
|
||||
const onInsertPillButton = function (): void {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
});
|
||||
};
|
||||
|
||||
const onInviteUserButton = async (ev: Event): Promise<void> => {
|
||||
try {
|
||||
const roomId =
|
||||
member instanceof RoomMember && member.roomId
|
||||
? member.roomId
|
||||
: SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
|
||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||
const inviter = new MultiInviter(cli, roomId || "");
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||
const errorStringFromInviterUtility = inviter.getErrorText(member.userId);
|
||||
if (errorStringFromInviterUtility) {
|
||||
throw new Error(errorStringFromInviterUtility);
|
||||
} else {
|
||||
throw new UserFriendlyError("slash_command|invite_failed", {
|
||||
user: member.userId,
|
||||
roomId,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("invite|failed_title"),
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoInviteButton", ev);
|
||||
};
|
||||
|
||||
const onShareUserClick = (): void => {
|
||||
Modal.createDialog(ShareDialog, {
|
||||
target: member,
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenDmForUser = async (user: Member): Promise<void> => {
|
||||
const avatarUrl = user instanceof User ? user.avatarUrl : user.getMxcAvatarUrl();
|
||||
const startDmUser = new DirectoryMember({
|
||||
user_id: user.userId,
|
||||
display_name: user.rawDisplayName,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
await startDmOnFirstMessage(cli, [startDmUser]);
|
||||
};
|
||||
|
||||
return {
|
||||
isMe,
|
||||
showInviteButton,
|
||||
showInsertPillButton,
|
||||
readReceiptButtonDisabled,
|
||||
onReadReceiptButton,
|
||||
onInsertPillButton,
|
||||
onInviteUserButton,
|
||||
onShareUserClick,
|
||||
onOpenDmForUser,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
EventType,
|
||||
type RoomMember,
|
||||
type IPowerLevelsContent,
|
||||
type Room,
|
||||
RoomStateEvent,
|
||||
type MatrixClient,
|
||||
type User,
|
||||
type MatrixEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||
import { useTypedEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||
import Modal from "../../../../Modal";
|
||||
import ErrorDialog from "../../../views/dialogs/ErrorDialog";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { type IRoomPermissions } from "../../../views/right_panel/UserInfo";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||
import QuestionDialog from "../../../views/dialogs/QuestionDialog";
|
||||
import DMRoomMap from "../../../../utils/DMRoomMap";
|
||||
|
||||
export interface UserInfoBasicState {
|
||||
// current room powerlevels
|
||||
powerLevels: IPowerLevelsContent;
|
||||
// getting user permissions in this room
|
||||
roomPermissions: IRoomPermissions;
|
||||
// numbers of operation in progress > 0
|
||||
pendingUpdateCount: number;
|
||||
// true if user is me
|
||||
isMe: boolean;
|
||||
// true if room is a DM for the user
|
||||
isRoomDMForMember: boolean;
|
||||
// Boolean to hide or show the deactivate button
|
||||
showDeactivateButton: boolean;
|
||||
// Method called when a deactivate user action is triggered
|
||||
onSynapseDeactivate: () => void;
|
||||
startUpdating: () => void;
|
||||
stopUpdating: () => void;
|
||||
}
|
||||
|
||||
export const getPowerLevels = (room: Room): IPowerLevelsContent =>
|
||||
room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPermissions = (cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions => {
|
||||
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
|
||||
const updateRoomPermissions = useCallback(() => {
|
||||
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
if (!powerLevels) return;
|
||||
|
||||
const me = room.getMember(cli.getUserId() || "");
|
||||
if (!me) return;
|
||||
|
||||
const them = user;
|
||||
const isMe = me.userId === them.userId;
|
||||
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||
|
||||
let modifyLevelMax = -1;
|
||||
if (canAffectUser) {
|
||||
const editPowerLevel = powerLevels.events?.[EventType.RoomPowerLevels] ?? powerLevels.state_default ?? 50;
|
||||
if (me.powerLevel >= editPowerLevel) {
|
||||
modifyLevelMax = me.powerLevel;
|
||||
}
|
||||
}
|
||||
|
||||
setRoomPermissions({
|
||||
canInvite: me.powerLevel >= (powerLevels.invite ?? 0),
|
||||
canEdit: modifyLevelMax >= 0,
|
||||
modifyLevelMax,
|
||||
});
|
||||
}, [cli, user, room]);
|
||||
|
||||
useTypedEventEmitter(cli, RoomStateEvent.Update, updateRoomPermissions);
|
||||
useEffect(() => {
|
||||
updateRoomPermissions();
|
||||
return () => {
|
||||
setRoomPermissions({
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
};
|
||||
}, [updateRoomPermissions]);
|
||||
|
||||
return roomPermissions;
|
||||
};
|
||||
|
||||
const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
|
||||
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
|
||||
};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room): IPowerLevelsContent => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
||||
const update = useCallback(
|
||||
(ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
setPowerLevels(getPowerLevels(room));
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
useTypedEventEmitter(cli, RoomStateEvent.Events, update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setPowerLevels({});
|
||||
};
|
||||
}, [update]);
|
||||
return powerLevels;
|
||||
};
|
||||
|
||||
export const useUserInfoBasicViewModel = (room: Room, member: User | RoomMember): UserInfoBasicState => {
|
||||
const cli = useMatrixClientContext();
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||
|
||||
// selected member is current user
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
// is needed to hide the Roles section for DMs as it doesn't make sense there
|
||||
const isRoomDMForMember = !!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId);
|
||||
|
||||
// used to check if user can deactivate another member
|
||||
const isMemberSameDomain = member.userId.endsWith(`:${cli.getDomain()}`);
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
const showDeactivateButton = isSynapseAdmin && isMemberSameDomain;
|
||||
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("user_info|deactivate_confirm_title"),
|
||||
description: <div>{_t("user_info|deactivate_confirm_description")}</div>,
|
||||
button: _t("user_info|deactivate_confirm_action"),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [accepted] = await finished;
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await cli.deactivateSynapseUser(member.userId);
|
||||
} catch (err) {
|
||||
logger.error("Failed to deactivate user");
|
||||
logger.error(err);
|
||||
|
||||
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("user_info|error_deactivate"),
|
||||
description,
|
||||
});
|
||||
}
|
||||
}, [cli, member.userId]);
|
||||
|
||||
return {
|
||||
showDeactivateButton,
|
||||
powerLevels,
|
||||
roomPermissions,
|
||||
pendingUpdateCount,
|
||||
isMe,
|
||||
isRoomDMForMember,
|
||||
onSynapseDeactivate,
|
||||
startUpdating,
|
||||
stopUpdating,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2025 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, { useContext, useEffect, useState, useCallback } from "react";
|
||||
import { type RoomMember, User, ClientEvent, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import Modal from "../../../../Modal";
|
||||
import QuestionDialog from "../../../views/dialogs/QuestionDialog";
|
||||
import { useTypedEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||
|
||||
export interface UserInfoPowerLevelState {
|
||||
/**
|
||||
* Weither the member is ignored by current user or not
|
||||
*/
|
||||
isIgnored: boolean;
|
||||
/**
|
||||
* Trigger the method to ignore or unignore a user
|
||||
* @param ev - The click event
|
||||
*/
|
||||
ignoreButtonClick: (ev: Event) => void;
|
||||
}
|
||||
|
||||
export const useUserInfoIgnoreButtonViewModel = (member: User | RoomMember): UserInfoPowerLevelState => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const unignore = useCallback(() => {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(member.userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
cli.setIgnoredUsers(ignoredUsers);
|
||||
}, [cli, member]);
|
||||
|
||||
const ignore = useCallback(async () => {
|
||||
const name = (member instanceof User ? member.displayName : member.name) || member.userId;
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("user_info|ignore_confirm_title", { user: name }),
|
||||
description: <div>{_t("user_info|ignore_confirm_description")}</div>,
|
||||
button: _t("action|ignore"),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
|
||||
if (confirmed) {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
ignoredUsers.push(member.userId);
|
||||
cli.setIgnoredUsers(ignoredUsers);
|
||||
}
|
||||
}, [cli, member]);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}, [cli, member.userId]);
|
||||
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback(
|
||||
(ev: MatrixEvent) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}
|
||||
},
|
||||
[cli, member.userId],
|
||||
);
|
||||
useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler);
|
||||
|
||||
const ignoreButtonClick = (ev: Event): void => {
|
||||
ev.preventDefault();
|
||||
if (isIgnored) {
|
||||
unignore();
|
||||
} else {
|
||||
ignore();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
ignoreButtonClick,
|
||||
isIgnored,
|
||||
};
|
||||
};
|
||||
@@ -9,62 +9,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { type JSX, type ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
ClientEvent,
|
||||
type MatrixClient,
|
||||
RoomMember,
|
||||
type Room,
|
||||
RoomStateEvent,
|
||||
type MatrixEvent,
|
||||
User,
|
||||
type Device,
|
||||
EventType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { type MatrixClient, type RoomMember, type Room, type User, type Device } from "matrix-js-sdk/src/matrix";
|
||||
import { type UserVerificationStatus, type VerificationRequest, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MenuItem } from "@vector-im/compound-web";
|
||||
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
|
||||
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
|
||||
import MentionIcon from "@vector-im/compound-design-tokens/assets/web/icons/mention";
|
||||
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
|
||||
import BlockIcon from "@vector-im/compound-design-tokens/assets/web/icons/block";
|
||||
import DeleteIcon from "@vector-im/compound-design-tokens/assets/web/icons/delete";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import Modal from "../../../Modal";
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
import BaseCard from "./BaseCard";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { ShareDialog } from "../dialogs/ShareDialog";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { type IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
|
||||
import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
|
||||
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
|
||||
import { UserInfoBasicView } from "./user_info/UserInfoBasicView";
|
||||
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
@@ -87,190 +50,6 @@ export const disambiguateDevices = (devices: IDevice[]): void => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the member to a DirectoryMember and starts a DM with them.
|
||||
*/
|
||||
async function openDmForUser(matrixClient: MatrixClient, user: Member): Promise<void> {
|
||||
const avatarUrl = user instanceof User ? user.avatarUrl : user.getMxcAvatarUrl();
|
||||
const startDmUser = new DirectoryMember({
|
||||
user_id: user.userId,
|
||||
display_name: user.rawDisplayName,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
await startDmOnFirstMessage(matrixClient, [startDmUser]);
|
||||
}
|
||||
|
||||
const MessageButton = ({ member }: { member: Member }): JSX.Element => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
await openDmForUser(cli, member);
|
||||
setBusy(false);
|
||||
}}
|
||||
disabled={busy}
|
||||
label={_t("user_info|send_message")}
|
||||
Icon={ChatIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserOptionsSection: React.FC<{
|
||||
member: Member;
|
||||
canInvite: boolean;
|
||||
isSpace?: boolean;
|
||||
children?: ReactNode;
|
||||
}> = ({ member, canInvite, isSpace, children }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let insertPillButton: JSX.Element | undefined;
|
||||
let inviteUserButton: JSX.Element | undefined;
|
||||
let readReceiptButton: JSX.Element | undefined;
|
||||
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
const onShareUserClick = (): void => {
|
||||
Modal.createDialog(ShareDialog, {
|
||||
target: member,
|
||||
});
|
||||
};
|
||||
|
||||
// Only allow the user to ignore the user if its not ourselves
|
||||
// same goes for jumping to read receipt
|
||||
if (!isMe) {
|
||||
const onReadReceiptButton = function (room: Room): void {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
highlighted: true,
|
||||
// this could return null, the default prevents a type error
|
||||
event_id: room.getEventReadUpTo(member.userId) || undefined,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined, // room doesn't change
|
||||
});
|
||||
};
|
||||
|
||||
const room = member instanceof RoomMember ? cli.getRoom(member.roomId) : null;
|
||||
const readReceiptButtonDisabled = isSpace || !room?.getEventReadUpTo(member.userId);
|
||||
readReceiptButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (room && !readReceiptButtonDisabled) {
|
||||
onReadReceiptButton(room);
|
||||
}
|
||||
}}
|
||||
label={_t("user_info|jump_to_rr_button")}
|
||||
disabled={readReceiptButtonDisabled}
|
||||
Icon={CheckIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (member instanceof RoomMember && member.roomId && !isSpace) {
|
||||
const onInsertPillButton = function (): void {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
userId: member.userId,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
});
|
||||
};
|
||||
|
||||
insertPillButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
onInsertPillButton();
|
||||
}}
|
||||
label={_t("action|mention")}
|
||||
Icon={MentionIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
member instanceof RoomMember &&
|
||||
canInvite &&
|
||||
(member?.membership ?? KnownMembership.Leave) === KnownMembership.Leave &&
|
||||
shouldShowComponent(UIComponent.InviteUsers)
|
||||
) {
|
||||
const roomId = member && member.roomId ? member.roomId : SdkContextClass.instance.roomViewStore.getRoomId();
|
||||
const onInviteUserButton = async (ev: Event): Promise<void> => {
|
||||
try {
|
||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||
const inviter = new MultiInviter(cli, roomId || "");
|
||||
await inviter.invite([member.userId]).then(() => {
|
||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||
const errorStringFromInviterUtility = inviter.getErrorText(member.userId);
|
||||
if (errorStringFromInviterUtility) {
|
||||
throw new Error(errorStringFromInviterUtility);
|
||||
} else {
|
||||
throw new UserFriendlyError("slash_command|invite_failed", {
|
||||
user: member.userId,
|
||||
roomId,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("invite|failed_title"),
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoInviteButton", ev);
|
||||
};
|
||||
|
||||
inviteUserButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
onInviteUserButton(ev);
|
||||
}}
|
||||
label={_t("action|invite")}
|
||||
Icon={InviteIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const shareUserButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
onShareUserClick();
|
||||
}}
|
||||
label={_t("user_info|share_button")}
|
||||
Icon={ShareIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const directMessageButton =
|
||||
isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : <MessageButton member={member} />;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{children}
|
||||
{directMessageButton}
|
||||
{inviteUserButton}
|
||||
{readReceiptButton}
|
||||
{shareUserButton}
|
||||
{insertPillButton}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("user_info|demote_self_confirm_title"),
|
||||
@@ -325,152 +104,12 @@ export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsConte
|
||||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
export const getPowerLevels = (room: Room): IPowerLevelsContent =>
|
||||
room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||
|
||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room): IPowerLevelsContent => {
|
||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||
|
||||
const update = useCallback(
|
||||
(ev?: MatrixEvent) => {
|
||||
if (!room) return;
|
||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||
setPowerLevels(getPowerLevels(room));
|
||||
},
|
||||
[room],
|
||||
);
|
||||
|
||||
useTypedEventEmitter(cli, RoomStateEvent.Events, update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
setPowerLevels({});
|
||||
};
|
||||
}, [update]);
|
||||
return powerLevels;
|
||||
};
|
||||
|
||||
const IgnoreToggleButton: React.FC<{
|
||||
member: User | RoomMember;
|
||||
}> = ({ member }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const unignore = useCallback(() => {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(member.userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
cli.setIgnoredUsers(ignoredUsers);
|
||||
}, [cli, member]);
|
||||
|
||||
const ignore = useCallback(async () => {
|
||||
const name = (member instanceof User ? member.displayName : member.name) || member.userId;
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("user_info|ignore_confirm_title", { user: name }),
|
||||
description: <div>{_t("user_info|ignore_confirm_description")}</div>,
|
||||
button: _t("action|ignore"),
|
||||
});
|
||||
const [confirmed] = await finished;
|
||||
|
||||
if (confirmed) {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
ignoredUsers.push(member.userId);
|
||||
cli.setIgnoredUsers(ignoredUsers);
|
||||
}
|
||||
}, [cli, member]);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(member.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}, [cli, member.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback(
|
||||
(ev: MatrixEvent) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(member.userId));
|
||||
}
|
||||
},
|
||||
[cli, member.userId],
|
||||
);
|
||||
useTypedEventEmitter(cli, ClientEvent.AccountData, accountDataHandler);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (isIgnored) {
|
||||
unignore();
|
||||
} else {
|
||||
ignore();
|
||||
}
|
||||
}}
|
||||
label={isIgnored ? _t("user_info|unignore_button") : _t("user_info|ignore_button")}
|
||||
kind="critical"
|
||||
Icon={BlockIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
|
||||
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
|
||||
};
|
||||
|
||||
export interface IRoomPermissions {
|
||||
modifyLevelMax: number;
|
||||
canEdit: boolean;
|
||||
canInvite: boolean;
|
||||
}
|
||||
|
||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
|
||||
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
|
||||
const updateRoomPermissions = useCallback(() => {
|
||||
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
if (!powerLevels) return;
|
||||
|
||||
const me = room.getMember(cli.getUserId() || "");
|
||||
if (!me) return;
|
||||
|
||||
const them = user;
|
||||
const isMe = me.userId === them.userId;
|
||||
const canAffectUser = them.powerLevel < me.powerLevel || isMe;
|
||||
|
||||
let modifyLevelMax = -1;
|
||||
if (canAffectUser) {
|
||||
const editPowerLevel = powerLevels.events?.[EventType.RoomPowerLevels] ?? powerLevels.state_default ?? 50;
|
||||
if (me.powerLevel >= editPowerLevel) {
|
||||
modifyLevelMax = me.powerLevel;
|
||||
}
|
||||
}
|
||||
|
||||
setRoomPermissions({
|
||||
canInvite: me.powerLevel >= (powerLevels.invite ?? 0),
|
||||
canEdit: modifyLevelMax >= 0,
|
||||
modifyLevelMax,
|
||||
});
|
||||
}, [cli, user, room]);
|
||||
|
||||
useTypedEventEmitter(cli, RoomStateEvent.Update, updateRoomPermissions);
|
||||
useEffect(() => {
|
||||
updateRoomPermissions();
|
||||
return () => {
|
||||
setRoomPermissions({
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
};
|
||||
}, [updateRoomPermissions]);
|
||||
|
||||
return roomPermissions;
|
||||
}
|
||||
|
||||
async function getUserDeviceInfo(
|
||||
userId: string,
|
||||
cli: MatrixClient,
|
||||
@@ -547,124 +186,6 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => {
|
||||
return devices;
|
||||
};
|
||||
|
||||
const BasicUserInfo: React.FC<{
|
||||
room: Room;
|
||||
member: User | RoomMember;
|
||||
}> = ({ room, member }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
title: _t("user_info|deactivate_confirm_title"),
|
||||
description: <div>{_t("user_info|deactivate_confirm_description")}</div>,
|
||||
button: _t("user_info|deactivate_confirm_action"),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [accepted] = await finished;
|
||||
if (!accepted) return;
|
||||
try {
|
||||
await cli.deactivateSynapseUser(member.userId);
|
||||
} catch (err) {
|
||||
logger.error("Failed to deactivate user");
|
||||
logger.error(err);
|
||||
|
||||
const description = err instanceof Error ? err.message : _t("invite|failed_generic");
|
||||
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("user_info|error_deactivate"),
|
||||
description,
|
||||
});
|
||||
}
|
||||
}, [cli, member.userId]);
|
||||
|
||||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
if (isSynapseAdmin && member.userId.endsWith(`:${cli.getDomain()}`)) {
|
||||
synapseDeactivateButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
onSynapseDeactivate();
|
||||
}}
|
||||
label={_t("user_info|deactivate_confirm_action")}
|
||||
kind="critical"
|
||||
Icon={DeleteIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let memberDetails;
|
||||
let adminToolsContainer;
|
||||
if (room && (member as RoomMember).roomId) {
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
|
||||
memberDetails = (
|
||||
<PowerLevelSection user={member as RoomMember} room={room} roomPermissions={roomPermissions} />
|
||||
);
|
||||
}
|
||||
|
||||
adminToolsContainer = (
|
||||
<UserInfoAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={member as RoomMember}
|
||||
room={room}
|
||||
isUpdating={pendingUpdateCount > 0}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}
|
||||
>
|
||||
{synapseDeactivateButton}
|
||||
</UserInfoAdminToolsContainer>
|
||||
);
|
||||
} else if (synapseDeactivateButton) {
|
||||
adminToolsContainer = <Container>{synapseDeactivateButton}</Container>;
|
||||
}
|
||||
|
||||
if (pendingUpdateCount > 0) {
|
||||
spinner = <Spinner />;
|
||||
}
|
||||
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
member={member as RoomMember}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
>
|
||||
{memberDetails}
|
||||
</UserOptionsSection>
|
||||
{adminToolsContainer}
|
||||
{!isMe && (
|
||||
<Container>
|
||||
<IgnoreToggleButton member={member} />
|
||||
</Container>
|
||||
)}
|
||||
{spinner}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export type Member = User | RoomMember;
|
||||
|
||||
interface IProps {
|
||||
@@ -700,7 +221,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||
let content: JSX.Element | undefined;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.MemberInfo:
|
||||
content = <BasicUserInfo room={room as Room} member={member as User} />;
|
||||
content = <UserInfoBasicView room={room as Room} member={member as User} />;
|
||||
break;
|
||||
case RightPanelPhases.EncryptionPanel:
|
||||
classes.push("mx_UserInfo_smallAvatar");
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type RoomMember, type User, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import React, { type JSX, type ReactNode, useState } from "react";
|
||||
import { MenuItem } from "@vector-im/compound-web";
|
||||
import { ChatIcon, CheckIcon, MentionIcon, ShareIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { useUserInfoBasicOptionsViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoBasicOptionsViewModel";
|
||||
import { Container, type Member } from "../UserInfo";
|
||||
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../settings/UIFeature";
|
||||
|
||||
const MessageButton = ({
|
||||
member,
|
||||
openDMForUser,
|
||||
}: {
|
||||
member: Member;
|
||||
openDMForUser: (user: Member) => Promise<void>;
|
||||
}): JSX.Element => {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
await openDMForUser(member);
|
||||
setBusy(false);
|
||||
}}
|
||||
disabled={busy}
|
||||
label={_t("user_info|send_message")}
|
||||
Icon={ChatIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserInfoBasicOptionsView: React.FC<{
|
||||
member: User | RoomMember;
|
||||
room: Room;
|
||||
children?: ReactNode;
|
||||
}> = ({ room, member, children }) => {
|
||||
const vm = useUserInfoBasicOptionsViewModel(room, member);
|
||||
|
||||
let insertPillButton: JSX.Element | undefined;
|
||||
let inviteUserButton: JSX.Element | undefined;
|
||||
let readReceiptButton: JSX.Element | undefined;
|
||||
|
||||
if (!vm.isMe) {
|
||||
readReceiptButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onReadReceiptButton();
|
||||
}}
|
||||
label={_t("user_info|jump_to_rr_button")}
|
||||
disabled={vm.readReceiptButtonDisabled}
|
||||
Icon={CheckIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (vm.showInsertPillButton) {
|
||||
insertPillButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onInsertPillButton();
|
||||
}}
|
||||
label={_t("action|mention")}
|
||||
Icon={MentionIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (vm.showInviteButton && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
inviteUserButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onInviteUserButton(ev);
|
||||
}}
|
||||
label={_t("action|invite")}
|
||||
Icon={InviteIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const shareUserButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onShareUserClick();
|
||||
}}
|
||||
label={_t("user_info|share_button")}
|
||||
Icon={ShareIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
const directMessageButton =
|
||||
vm.isMe || !shouldShowComponent(UIComponent.CreateRooms) ? null : (
|
||||
<MessageButton member={member} openDMForUser={vm.onOpenDmForUser} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{children}
|
||||
{directMessageButton}
|
||||
{inviteUserButton}
|
||||
{readReceiptButton}
|
||||
{shareUserButton}
|
||||
{insertPillButton}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { type RoomMember, type User, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { MenuItem } from "@vector-im/compound-web";
|
||||
import { DeleteIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { useUserInfoBasicViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoBasicViewModel";
|
||||
import { PowerLevelSection } from "./UserInfoPowerLevels";
|
||||
import { Container } from "../UserInfo";
|
||||
import { IgnoreToggleButton } from "./UserInfoIgnoreButtonView";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import { UserInfoAdminToolsContainer } from "./UserInfoAdminToolsContainer";
|
||||
import { UserInfoBasicOptionsView } from "./UserInfoBasicOptionsView";
|
||||
|
||||
/**
|
||||
* There are two types of components that can be displayed in the right panel concerning userinfo
|
||||
* Basic info or Encryption Panel
|
||||
*/
|
||||
export const UserInfoBasicView: React.FC<{
|
||||
room: Room;
|
||||
member: User | RoomMember;
|
||||
}> = ({ room, member }) => {
|
||||
const vm = useUserInfoBasicViewModel(room, member);
|
||||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
let memberDetails;
|
||||
let adminToolsContainer;
|
||||
|
||||
if (vm.showDeactivateButton) {
|
||||
synapseDeactivateButton = (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => {
|
||||
ev.preventDefault();
|
||||
vm.onSynapseDeactivate();
|
||||
}}
|
||||
label={_t("user_info|deactivate_confirm_action")}
|
||||
kind="critical"
|
||||
Icon={DeleteIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (room && (member as RoomMember).roomId) {
|
||||
// hide the Roles section for DMs as it doesn't make sense there
|
||||
if (!vm.isRoomDMForMember) {
|
||||
memberDetails = (
|
||||
<PowerLevelSection user={member as RoomMember} room={room} roomPermissions={vm.roomPermissions} />
|
||||
);
|
||||
}
|
||||
|
||||
adminToolsContainer = (
|
||||
<UserInfoAdminToolsContainer
|
||||
powerLevels={vm.powerLevels}
|
||||
member={member as RoomMember}
|
||||
room={room}
|
||||
isUpdating={vm.pendingUpdateCount > 0}
|
||||
startUpdating={vm.startUpdating}
|
||||
stopUpdating={vm.stopUpdating}
|
||||
>
|
||||
{synapseDeactivateButton}
|
||||
</UserInfoAdminToolsContainer>
|
||||
);
|
||||
} else if (synapseDeactivateButton) {
|
||||
adminToolsContainer = <Container>{synapseDeactivateButton}</Container>;
|
||||
}
|
||||
|
||||
if (vm.pendingUpdateCount > 0) {
|
||||
spinner = <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<UserInfoBasicOptionsView room={room} member={member}>
|
||||
{memberDetails}
|
||||
</UserInfoBasicOptionsView>
|
||||
{adminToolsContainer}
|
||||
{!vm.isMe && (
|
||||
<Container>
|
||||
<IgnoreToggleButton member={member} />
|
||||
</Container>
|
||||
)}
|
||||
{spinner}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type RoomMember, type User } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import { MenuItem } from "@vector-im/compound-web";
|
||||
import { BlockIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { useUserInfoIgnoreButtonViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoIgnoreButtonViewModel";
|
||||
|
||||
export const IgnoreToggleButton: React.FC<{
|
||||
member: User | RoomMember;
|
||||
}> = ({ member }) => {
|
||||
const vm = useUserInfoIgnoreButtonViewModel(member);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
role="button"
|
||||
onSelect={async (ev) => vm.ignoreButtonClick(ev)}
|
||||
label={vm.isIgnored ? _t("user_info|unignore_button") : _t("user_info|ignore_button")}
|
||||
kind="critical"
|
||||
Icon={BlockIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user