matrix-react-sdk
Version:
SDK for matrix.org using React
1,146 lines (1,130 loc) • 238 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.BanToggleButton = void 0;
exports.DeviceItem = DeviceItem;
exports.warnSelfDemote = exports.useRoomPowerLevels = exports.useDevices = exports.isMuted = exports.getPowerLevels = exports.getE2EStatus = exports.disambiguateDevices = exports.default = exports.UserOptionsSection = exports.UserInfoHeader = exports.RoomKickButton = exports.RoomAdminToolsContainer = exports.PowerLevelEditor = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireWildcard(require("react"));
var _classnames = _interopRequireDefault(require("classnames"));
var _matrix = require("matrix-js-sdk/src/matrix");
var _types = require("matrix-js-sdk/src/types");
var _logger = require("matrix-js-sdk/src/logger");
var _crypto = require("matrix-js-sdk/src/crypto");
var _compoundWeb = require("@vector-im/compound-web");
var _chat = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/chat"));
var _check = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/check"));
var _share = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/share"));
var _mention = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/mention"));
var _userAdd = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/user-add"));
var _block = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/block"));
var _delete = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/delete"));
var _close = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/close"));
var _chatProblem = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/chat-problem"));
var _visibilityOff = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/visibility-off"));
var _leave = _interopRequireDefault(require("@vector-im/compound-design-tokens/assets/web/icons/leave"));
var _dispatcher = _interopRequireDefault(require("../../../dispatcher/dispatcher"));
var _Modal = _interopRequireDefault(require("../../../Modal"));
var _languageHandler = require("../../../languageHandler");
var _DMRoomMap = _interopRequireDefault(require("../../../utils/DMRoomMap"));
var _AccessibleButton = _interopRequireDefault(require("../elements/AccessibleButton"));
var _SdkConfig = _interopRequireDefault(require("../../../SdkConfig"));
var _MultiInviter = _interopRequireDefault(require("../../../utils/MultiInviter"));
var _E2EIcon = _interopRequireDefault(require("../rooms/E2EIcon"));
var _useEventEmitter = require("../../../hooks/useEventEmitter");
var _Roles = require("../../../Roles");
var _MatrixClientContext = _interopRequireDefault(require("../../../contexts/MatrixClientContext"));
var _RightPanelStorePhases = require("../../../stores/right-panel/RightPanelStorePhases");
var _EncryptionPanel = _interopRequireDefault(require("./EncryptionPanel"));
var _useAsyncMemo = require("../../../hooks/useAsyncMemo");
var _verification = require("../../../verification");
var _actions = require("../../../dispatcher/actions");
var _useIsEncrypted = require("../../../hooks/useIsEncrypted");
var _BaseCard = _interopRequireDefault(require("./BaseCard"));
var _ShieldUtils = require("../../../utils/ShieldUtils");
var _ImageView = _interopRequireDefault(require("../elements/ImageView"));
var _Spinner = _interopRequireDefault(require("../elements/Spinner"));
var _PowerSelector = _interopRequireDefault(require("../elements/PowerSelector"));
var _MemberAvatar = _interopRequireDefault(require("../avatars/MemberAvatar"));
var _PresenceLabel = _interopRequireDefault(require("../rooms/PresenceLabel"));
var _BulkRedactDialog = _interopRequireDefault(require("../dialogs/BulkRedactDialog"));
var _ShareDialog = _interopRequireDefault(require("../dialogs/ShareDialog"));
var _ErrorDialog = _interopRequireDefault(require("../dialogs/ErrorDialog"));
var _QuestionDialog = _interopRequireDefault(require("../dialogs/QuestionDialog"));
var _ConfirmUserActionDialog = _interopRequireDefault(require("../dialogs/ConfirmUserActionDialog"));
var _Media = require("../../../customisations/Media");
var _ConfirmSpaceUserActionDialog = _interopRequireDefault(require("../dialogs/ConfirmSpaceUserActionDialog"));
var _space = require("../../../utils/space");
var _UIComponents = require("../../../customisations/helpers/UIComponents");
var _UIFeature = require("../../../settings/UIFeature");
var _RoomContext = require("../../../contexts/RoomContext");
var _RightPanelStore = _interopRequireDefault(require("../../../stores/right-panel/RightPanelStore"));
var _UserIdentifier = _interopRequireDefault(require("../../../customisations/UserIdentifier"));
var _PosthogTrackers = _interopRequireDefault(require("../../../PosthogTrackers"));
var _directMessages = require("../../../utils/direct-messages");
var _SDKContext = require("../../../contexts/SDKContext");
var _arrays = require("../../../utils/arrays");
var _Flex = require("../../utils/Flex");
var _CopyableText = _interopRequireDefault(require("../elements/CopyableText"));
var _useUserTimezone = require("../../../hooks/useUserTimezone");
const _excluded = ["user", "room", "onClose", "phase"];
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /*
Copyright 2024 New Vector Ltd.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2017, 2018 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
const disambiguateDevices = devices => {
const names = Object.create(null);
for (let i = 0; i < devices.length; i++) {
const name = devices[i].displayName ?? "";
const indexList = names[name] || [];
indexList.push(i);
names[name] = indexList;
}
for (const name in names) {
if (names[name].length > 1) {
names[name].forEach(j => {
devices[j].ambiguous = true;
});
}
}
};
exports.disambiguateDevices = disambiguateDevices;
const getE2EStatus = async (cli, userId, devices) => {
const crypto = cli.getCrypto();
if (!crypto) return undefined;
const isMe = userId === cli.getUserId();
const userTrust = await crypto.getUserVerificationStatus(userId);
if (!userTrust.isCrossSigningVerified()) {
return userTrust.wasCrossSigningVerified() ? _ShieldUtils.E2EStatus.Warning : _ShieldUtils.E2EStatus.Normal;
}
const anyDeviceUnverified = await (0, _arrays.asyncSome)(devices, async device => {
const {
deviceId
} = device;
// 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 crypto.getDeviceVerificationStatus(userId, deviceId);
return isMe ? !deviceTrust?.crossSigningVerified : !deviceTrust?.isVerified();
});
return anyDeviceUnverified ? _ShieldUtils.E2EStatus.Warning : _ShieldUtils.E2EStatus.Verified;
};
/**
* Converts the member to a DirectoryMember and starts a DM with them.
*/
exports.getE2EStatus = getE2EStatus;
async function openDmForUser(matrixClient, user) {
const avatarUrl = user instanceof _matrix.User ? user.avatarUrl : user.getMxcAvatarUrl();
const startDmUser = new _directMessages.DirectoryMember({
user_id: user.userId,
display_name: user.rawDisplayName,
avatar_url: avatarUrl
});
await (0, _directMessages.startDmOnFirstMessage)(matrixClient, [startDmUser]);
}
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
return (0, _useAsyncMemo.useAsyncMemo)(async () => {
if (!canVerify) {
return undefined;
}
setUpdating(true);
try {
return await cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
} finally {
setUpdating(false);
}
}, [cli, member, canVerify]);
}
/**
* Display one device and the related actions
* @param userId current user id
* @param device device to display
* @param isUserVerified false when the user is not verified
* @constructor
*/
function DeviceItem({
userId,
device,
isUserVerified
}) {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const isMe = userId === cli.getUserId();
/** is the device verified? */
const isVerified = (0, _useAsyncMemo.useAsyncMemo)(async () => {
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, device.deviceId);
if (!deviceTrust) return false;
// 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.
return isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified();
}, [cli, userId, device]);
const classes = (0, _classnames.default)("mx_UserInfo_device", {
mx_UserInfo_device_verified: isVerified,
mx_UserInfo_device_unverified: !isVerified
});
const iconClasses = (0, _classnames.default)("mx_E2EIcon", {
mx_E2EIcon_normal: !isUserVerified,
mx_E2EIcon_verified: isVerified,
mx_E2EIcon_warning: isUserVerified && !isVerified
});
const onDeviceClick = () => {
const user = cli.getUser(userId);
if (user) {
(0, _verification.verifyDevice)(cli, user, device);
}
};
let deviceName;
if (!device.displayName?.trim()) {
deviceName = device.deviceId;
} else {
deviceName = device.ambiguous ? device.displayName + " (" + device.deviceId + ")" : device.displayName;
}
let trustedLabel;
if (isUserVerified) trustedLabel = isVerified ? (0, _languageHandler._t)("common|trusted") : (0, _languageHandler._t)("common|not_trusted");
if (isVerified === undefined) {
// we're still deciding if the device is verified
return /*#__PURE__*/_react.default.createElement("div", {
className: classes,
title: device.deviceId
});
} else if (isVerified) {
return /*#__PURE__*/_react.default.createElement("div", {
className: classes,
title: device.deviceId
}, /*#__PURE__*/_react.default.createElement("div", {
className: iconClasses
}), /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_device_name"
}, deviceName), /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_device_trusted"
}, trustedLabel));
} else {
return /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, {
className: classes,
title: device.deviceId,
"aria-label": deviceName,
onClick: onDeviceClick
}, /*#__PURE__*/_react.default.createElement("div", {
className: iconClasses
}), /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_device_name"
}, deviceName), /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_device_trusted"
}, trustedLabel));
}
}
/**
* Display a list of devices
* @param devices devices to display
* @param userId current user id
* @param loading displays a spinner instead of the device section
* @param isUserVerified is false when
* - the user is not verified, or
* - `MatrixClient.getCrypto.getUserVerificationStatus` async call is in progress (in which case `loading` will also be `true`)
* @constructor
*/
function DevicesSection({
devices,
userId,
loading,
isUserVerified
}) {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const [isExpanded, setExpanded] = (0, _react.useState)(false);
const deviceTrusts = (0, _useAsyncMemo.useAsyncMemo)(() => {
const cryptoApi = cli.getCrypto();
if (!cryptoApi) return Promise.resolve(undefined);
return Promise.all(devices.map(d => cryptoApi.getDeviceVerificationStatus(userId, d.deviceId)));
}, [cli, userId, devices]);
if (loading || deviceTrusts === undefined) {
// still loading
return /*#__PURE__*/_react.default.createElement(_Spinner.default, null);
}
const isMe = userId === cli.getUserId();
let expandSectionDevices = [];
const unverifiedDevices = [];
let expandCountCaption;
let expandHideCaption;
let expandIconClasses = "mx_E2EIcon";
const dehydratedDeviceIds = [];
for (const device of devices) {
if (device.dehydrated) {
dehydratedDeviceIds.push(device.deviceId);
}
}
// If the user has exactly one device marked as dehydrated, we consider
// that as the dehydrated device, and hide it as a normal device (but
// indicate that the user is using a dehydrated device). If the user has
// more than one, that is anomalous, and we show all the devices so that
// nothing is hidden.
const dehydratedDeviceId = dehydratedDeviceIds.length == 1 ? dehydratedDeviceIds[0] : undefined;
let dehydratedDeviceInExpandSection = false;
if (isUserVerified) {
for (let i = 0; i < devices.length; ++i) {
const device = devices[i];
const deviceTrust = deviceTrusts[i];
// 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 isVerified = deviceTrust && (isMe ? deviceTrust.crossSigningVerified : deviceTrust.isVerified());
if (isVerified) {
// don't show dehydrated device as a normal device, if it's
// verified
if (device.deviceId === dehydratedDeviceId) {
dehydratedDeviceInExpandSection = true;
} else {
expandSectionDevices.push(device);
}
} else {
unverifiedDevices.push(device);
}
}
expandCountCaption = (0, _languageHandler._t)("user_info|count_of_verified_sessions", {
count: expandSectionDevices.length
});
expandHideCaption = (0, _languageHandler._t)("user_info|hide_verified_sessions");
expandIconClasses += " mx_E2EIcon_verified";
} else {
if (dehydratedDeviceId) {
devices = devices.filter(device => device.deviceId !== dehydratedDeviceId);
dehydratedDeviceInExpandSection = true;
}
expandSectionDevices = devices;
expandCountCaption = (0, _languageHandler._t)("user_info|count_of_sessions", {
count: devices.length
});
expandHideCaption = (0, _languageHandler._t)("user_info|hide_sessions");
expandIconClasses += " mx_E2EIcon_normal";
}
let expandButton;
if (expandSectionDevices.length) {
if (isExpanded) {
expandButton = /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, {
kind: "link",
className: "mx_UserInfo_expand",
onClick: () => setExpanded(false)
}, /*#__PURE__*/_react.default.createElement("div", null, expandHideCaption));
} else {
expandButton = /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, {
kind: "link",
className: "mx_UserInfo_expand",
onClick: () => setExpanded(true)
}, /*#__PURE__*/_react.default.createElement("div", {
className: expandIconClasses
}), /*#__PURE__*/_react.default.createElement("div", null, expandCountCaption));
}
}
let deviceList = unverifiedDevices.map((device, i) => {
return /*#__PURE__*/_react.default.createElement(DeviceItem, {
key: i,
userId: userId,
device: device,
isUserVerified: isUserVerified
});
});
if (isExpanded) {
const keyStart = unverifiedDevices.length;
deviceList = deviceList.concat(expandSectionDevices.map((device, i) => {
return /*#__PURE__*/_react.default.createElement(DeviceItem, {
key: i + keyStart,
userId: userId,
device: device,
isUserVerified: isUserVerified
});
}));
if (dehydratedDeviceInExpandSection) {
deviceList.push( /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("user_info|dehydrated_device_enabled")));
}
}
return /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_devices"
}, /*#__PURE__*/_react.default.createElement("div", null, deviceList), /*#__PURE__*/_react.default.createElement("div", null, expandButton));
}
const MessageButton = ({
member
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const [busy, setBusy] = (0, _react.useState)(false);
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
if (busy) return;
setBusy(true);
await openDmForUser(cli, member);
setBusy(false);
},
disabled: busy,
label: (0, _languageHandler._t)("user_info|send_message"),
Icon: _chat.default
});
};
const UserOptionsSection = ({
member,
canInvite,
isSpace,
children
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
let insertPillButton;
let inviteUserButton;
let readReceiptButton;
const isMe = member.userId === cli.getUserId();
const onShareUserClick = () => {
_Modal.default.createDialog(_ShareDialog.default, {
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) {
_dispatcher.default.dispatch({
action: _actions.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 _matrix.RoomMember ? cli.getRoom(member.roomId) : null;
const readReceiptButtonDisabled = isSpace || !room?.getEventReadUpTo(member.userId);
readReceiptButton = /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
if (room && !readReceiptButtonDisabled) {
onReadReceiptButton(room);
}
},
label: (0, _languageHandler._t)("user_info|jump_to_rr_button"),
disabled: readReceiptButtonDisabled,
Icon: _check.default
});
if (member instanceof _matrix.RoomMember && member.roomId && !isSpace) {
const onInsertPillButton = function () {
_dispatcher.default.dispatch({
action: _actions.Action.ComposerInsert,
userId: member.userId,
timelineRenderingType: _RoomContext.TimelineRenderingType.Room
});
};
insertPillButton = /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onInsertPillButton();
},
label: (0, _languageHandler._t)("action|mention"),
Icon: _mention.default
});
}
if (member instanceof _matrix.RoomMember && canInvite && (member?.membership ?? _types.KnownMembership.Leave) === _types.KnownMembership.Leave && (0, _UIComponents.shouldShowComponent)(_UIFeature.UIComponent.InviteUsers)) {
const roomId = member && member.roomId ? member.roomId : _SDKContext.SdkContextClass.instance.roomViewStore.getRoomId();
const onInviteUserButton = async ev => {
try {
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
const inviter = new _MultiInviter.default(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 _languageHandler.UserFriendlyError("slash_command|invite_failed", {
user: member.userId,
roomId,
cause: undefined
});
}
}
});
} catch (err) {
const description = err instanceof Error ? err.message : (0, _languageHandler._t)("invite|failed_generic");
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("invite|failed_title"),
description
});
}
_PosthogTrackers.default.trackInteraction("WebRightPanelRoomUserInfoInviteButton", ev);
};
inviteUserButton = /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onInviteUserButton(ev);
},
label: (0, _languageHandler._t)("action|invite"),
Icon: _userAdd.default
});
}
}
const shareUserButton = /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onShareUserClick();
},
label: (0, _languageHandler._t)("user_info|share_button"),
Icon: _share.default
});
const directMessageButton = isMe || !(0, _UIComponents.shouldShowComponent)(_UIFeature.UIComponent.CreateRooms) ? null : /*#__PURE__*/_react.default.createElement(MessageButton, {
member: member
});
return /*#__PURE__*/_react.default.createElement(Container, null, children, directMessageButton, inviteUserButton, readReceiptButton, shareUserButton, insertPillButton);
};
exports.UserOptionsSection = UserOptionsSection;
const warnSelfDemote = async isSpace => {
const {
finished
} = _Modal.default.createDialog(_QuestionDialog.default, {
title: (0, _languageHandler._t)("user_info|demote_self_confirm_title"),
description: /*#__PURE__*/_react.default.createElement("div", null, isSpace ? (0, _languageHandler._t)("user_info|demote_self_confirm_description_space") : (0, _languageHandler._t)("user_info|demote_self_confirm_room")),
button: (0, _languageHandler._t)("user_info|demote_button")
});
const [confirmed] = await finished;
return !!confirmed;
};
exports.warnSelfDemote = warnSelfDemote;
const Container = ({
children
}) => {
return /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_container"
}, children);
};
const isMuted = (member, powerLevelContent) => {
if (!powerLevelContent || !member) return false;
const levelToSend = (powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) || powerLevelContent.events_default;
// levelToSend could be undefined as .events_default is optional. Coercing in this case using
// Number() would always return false, so this preserves behaviour
// FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
// the member has a negative powerlevel, this will give an incorrect result.
if (levelToSend === undefined) return false;
return member.powerLevel < levelToSend;
};
exports.isMuted = isMuted;
const getPowerLevels = room => room?.currentState?.getStateEvents(_matrix.EventType.RoomPowerLevels, "")?.getContent() || {};
exports.getPowerLevels = getPowerLevels;
const useRoomPowerLevels = (cli, room) => {
const [powerLevels, setPowerLevels] = (0, _react.useState)(getPowerLevels(room));
const update = (0, _react.useCallback)(ev => {
if (!room) return;
if (ev && ev.getType() !== _matrix.EventType.RoomPowerLevels) return;
setPowerLevels(getPowerLevels(room));
}, [room]);
(0, _useEventEmitter.useTypedEventEmitter)(cli, _matrix.RoomStateEvent.Events, update);
(0, _react.useEffect)(() => {
update();
return () => {
setPowerLevels({});
};
}, [update]);
return powerLevels;
};
exports.useRoomPowerLevels = useRoomPowerLevels;
const RoomKickButton = ({
room,
member,
isUpdating,
startUpdating,
stopUpdating
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
// check if user can be kicked/disinvited
if (member.membership !== _types.KnownMembership.Invite && member.membership !== _types.KnownMembership.Join) return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null);
const onKick = async () => {
if (isUpdating) return; // only allow one operation at a time
startUpdating();
const commonProps = {
member,
action: room.isSpaceRoom() ? member.membership === _types.KnownMembership.Invite ? (0, _languageHandler._t)("user_info|disinvite_button_space") : (0, _languageHandler._t)("user_info|kick_button_space") : member.membership === _types.KnownMembership.Invite ? (0, _languageHandler._t)("user_info|disinvite_button_room") : (0, _languageHandler._t)("user_info|kick_button_room"),
title: member.membership === _types.KnownMembership.Invite ? (0, _languageHandler._t)("user_info|disinvite_button_room_name", {
roomName: room.name
}) : (0, _languageHandler._t)("user_info|kick_button_room_name", {
roomName: room.name
}),
askReason: member.membership === _types.KnownMembership.Join,
danger: true
};
let finished;
if (room.isSpaceRoom()) {
({
finished
} = _Modal.default.createDialog(_ConfirmSpaceUserActionDialog.default, _objectSpread(_objectSpread({}, commonProps), {}, {
space: room,
spaceChildFilter: child => {
// Return true if the target member is not banned and we have sufficient PL to ban them
const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return !!myMember && !!theirMember && theirMember.membership === member.membership && myMember.powerLevel > theirMember.powerLevel && child.currentState.hasSufficientPowerLevelFor("kick", myMember.powerLevel);
},
allLabel: (0, _languageHandler._t)("user_info|kick_button_space_everything"),
specificLabel: (0, _languageHandler._t)("user_info|kick_space_specific"),
warningMessage: (0, _languageHandler._t)("user_info|kick_space_warning")
}), "mx_ConfirmSpaceUserActionDialog_wrapper"));
} else {
({
finished
} = _Modal.default.createDialog(_ConfirmUserActionDialog.default, commonProps));
}
const [proceed, reason, rooms = []] = await finished;
if (!proceed) {
stopUpdating();
return;
}
(0, _space.bulkSpaceBehaviour)(room, rooms, room => cli.kick(room.roomId, member.userId, reason || undefined)).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
_logger.logger.log("Kick success");
}, function (err) {
_logger.logger.error("Kick error: " + err);
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("user_info|error_kicking_user"),
description: err && err.message ? err.message : "Operation failed"
});
}).finally(() => {
stopUpdating();
});
};
const kickLabel = room.isSpaceRoom() ? member.membership === _types.KnownMembership.Invite ? (0, _languageHandler._t)("user_info|disinvite_button_space") : (0, _languageHandler._t)("user_info|kick_button_space") : member.membership === _types.KnownMembership.Invite ? (0, _languageHandler._t)("user_info|disinvite_button_room") : (0, _languageHandler._t)("user_info|kick_button_room");
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onKick();
},
disabled: isUpdating,
label: kickLabel,
kind: "critical",
Icon: _leave.default
});
};
exports.RoomKickButton = RoomKickButton;
const RedactMessagesButton = ({
member
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const onRedactAllMessages = () => {
const room = cli.getRoom(member.roomId);
if (!room) return;
_Modal.default.createDialog(_BulkRedactDialog.default, {
matrixClient: cli,
room,
member
});
};
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onRedactAllMessages();
},
label: (0, _languageHandler._t)("user_info|redact_button"),
kind: "critical",
Icon: _close.default
});
};
const BanToggleButton = ({
room,
member,
isUpdating,
startUpdating,
stopUpdating
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const isBanned = member.membership === _types.KnownMembership.Ban;
const onBanOrUnban = async () => {
if (isUpdating) return; // only allow one operation at a time
startUpdating();
const commonProps = {
member,
action: room.isSpaceRoom() ? isBanned ? (0, _languageHandler._t)("user_info|unban_button_space") : (0, _languageHandler._t)("user_info|ban_button_space") : isBanned ? (0, _languageHandler._t)("user_info|unban_button_room") : (0, _languageHandler._t)("user_info|ban_button_room"),
title: isBanned ? (0, _languageHandler._t)("user_info|unban_room_confirm_title", {
roomName: room.name
}) : (0, _languageHandler._t)("user_info|ban_room_confirm_title", {
roomName: room.name
}),
askReason: !isBanned,
danger: !isBanned
};
let finished;
if (room.isSpaceRoom()) {
({
finished
} = _Modal.default.createDialog(_ConfirmSpaceUserActionDialog.default, _objectSpread(_objectSpread({}, commonProps), {}, {
space: room,
spaceChildFilter: isBanned ? child => {
// Return true if the target member is banned and we have sufficient PL to unban
const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return !!myMember && !!theirMember && theirMember.membership === _types.KnownMembership.Ban && myMember.powerLevel > theirMember.powerLevel && child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
} : child => {
// Return true if the target member isn't banned and we have sufficient PL to ban
const myMember = child.getMember(cli.credentials.userId || "");
const theirMember = child.getMember(member.userId);
return !!myMember && !!theirMember && theirMember.membership !== _types.KnownMembership.Ban && myMember.powerLevel > theirMember.powerLevel && child.currentState.hasSufficientPowerLevelFor("ban", myMember.powerLevel);
},
allLabel: isBanned ? (0, _languageHandler._t)("user_info|unban_space_everything") : (0, _languageHandler._t)("user_info|ban_space_everything"),
specificLabel: isBanned ? (0, _languageHandler._t)("user_info|unban_space_specific") : (0, _languageHandler._t)("user_info|ban_space_specific"),
warningMessage: isBanned ? (0, _languageHandler._t)("user_info|unban_space_warning") : (0, _languageHandler._t)("user_info|kick_space_warning")
}), "mx_ConfirmSpaceUserActionDialog_wrapper"));
} else {
({
finished
} = _Modal.default.createDialog(_ConfirmUserActionDialog.default, commonProps));
}
const [proceed, reason, rooms = []] = await finished;
if (!proceed) {
stopUpdating();
return;
}
const fn = roomId => {
if (isBanned) {
return cli.unban(roomId, member.userId);
} else {
return cli.ban(roomId, member.userId, reason || undefined);
}
};
(0, _space.bulkSpaceBehaviour)(room, rooms, room => fn(room.roomId)).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
_logger.logger.log("Ban success");
}, function (err) {
_logger.logger.error("Ban error: " + err);
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("common|error"),
description: (0, _languageHandler._t)("user_info|error_ban_user")
});
}).finally(() => {
stopUpdating();
});
};
let label = room.isSpaceRoom() ? (0, _languageHandler._t)("user_info|ban_button_space") : (0, _languageHandler._t)("user_info|ban_button_room");
if (isBanned) {
label = room.isSpaceRoom() ? (0, _languageHandler._t)("user_info|unban_button_space") : (0, _languageHandler._t)("user_info|unban_button_room");
}
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onBanOrUnban();
},
disabled: isUpdating,
label: label,
kind: "critical",
Icon: _chatProblem.default
});
};
exports.BanToggleButton = BanToggleButton;
// We do not show a Mute button for ourselves so it doesn't need to handle warning self demotion
const MuteToggleButton = ({
member,
room,
powerLevels,
isUpdating,
startUpdating,
stopUpdating
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
// Don't show the mute/unmute option if the user is not in the room
if (member.membership !== _types.KnownMembership.Join) return null;
const muted = isMuted(member, powerLevels);
const onMuteToggle = async () => {
if (isUpdating) return; // only allow one operation at a time
startUpdating();
const roomId = member.roomId;
const target = member.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevels = powerLevelEvent?.getContent();
const levelToSend = powerLevels?.events?.["m.room.message"] ?? powerLevels?.events_default;
let level;
if (muted) {
// unmute
level = levelToSend;
} else {
// mute
level = levelToSend - 1;
}
level = parseInt(level);
if (isNaN(level)) {
stopUpdating();
return;
}
cli.setPowerLevel(roomId, target, level).then(() => {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
_logger.logger.log("Mute toggle success");
}, function (err) {
_logger.logger.error("Mute error: " + err);
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("common|error"),
description: (0, _languageHandler._t)("user_info|error_mute_user")
});
}).finally(() => {
stopUpdating();
});
};
const muteLabel = muted ? (0, _languageHandler._t)("common|unmute") : (0, _languageHandler._t)("common|mute");
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
onMuteToggle();
},
disabled: isUpdating,
label: muteLabel,
kind: "critical",
Icon: _visibilityOff.default
});
};
const IgnoreToggleButton = ({
member
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const unignore = (0, _react.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 = (0, _react.useCallback)(async () => {
const name = (member instanceof _matrix.User ? member.displayName : member.name) || member.userId;
const {
finished
} = _Modal.default.createDialog(_QuestionDialog.default, {
title: (0, _languageHandler._t)("user_info|ignore_confirm_title", {
user: name
}),
description: /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("user_info|ignore_confirm_description")),
button: (0, _languageHandler._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] = (0, _react.useState)(cli.isUserIgnored(member.userId));
// Recheck if the user or client changes
(0, _react.useEffect)(() => {
setIsIgnored(cli.isUserIgnored(member.userId));
}, [cli, member.userId]);
// Recheck also if we receive new accountData m.ignored_user_list
const accountDataHandler = (0, _react.useCallback)(ev => {
if (ev.getType() === "m.ignored_user_list") {
setIsIgnored(cli.isUserIgnored(member.userId));
}
}, [cli, member.userId]);
(0, _useEventEmitter.useTypedEventEmitter)(cli, _matrix.ClientEvent.AccountData, accountDataHandler);
return /*#__PURE__*/_react.default.createElement(_compoundWeb.MenuItem, {
role: "button",
onSelect: async ev => {
ev.preventDefault();
if (isIgnored) {
unignore();
} else {
ignore();
}
},
label: isIgnored ? (0, _languageHandler._t)("user_info|unignore_button") : (0, _languageHandler._t)("user_info|ignore_button"),
kind: "critical",
Icon: _block.default
});
};
const RoomAdminToolsContainer = ({
room,
children,
member,
isUpdating,
startUpdating,
stopUpdating,
powerLevels
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
let kickButton;
let banButton;
let muteButton;
let redactButton;
const editPowerLevel = (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default;
// if these do not exist in the event then they should default to 50 as per the spec
const {
ban: banPowerLevel = 50,
kick: kickPowerLevel = 50,
redact: redactPowerLevel = 50
} = powerLevels;
const me = room.getMember(cli.getUserId() || "");
if (!me) {
// we aren't in the room, so return no admin tooling
return /*#__PURE__*/_react.default.createElement("div", null);
}
const isMe = me.userId === member.userId;
const canAffectUser = member.powerLevel < me.powerLevel || isMe;
if (!isMe && canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = /*#__PURE__*/_react.default.createElement(RoomKickButton, {
room: room,
member: member,
isUpdating: isUpdating,
startUpdating: startUpdating,
stopUpdating: stopUpdating
});
}
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
redactButton = /*#__PURE__*/_react.default.createElement(RedactMessagesButton, {
member: member,
isUpdating: isUpdating,
startUpdating: startUpdating,
stopUpdating: stopUpdating
});
}
if (!isMe && canAffectUser && me.powerLevel >= banPowerLevel) {
banButton = /*#__PURE__*/_react.default.createElement(BanToggleButton, {
room: room,
member: member,
isUpdating: isUpdating,
startUpdating: startUpdating,
stopUpdating: stopUpdating
});
}
if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
muteButton = /*#__PURE__*/_react.default.createElement(MuteToggleButton, {
member: member,
room: room,
powerLevels: powerLevels,
isUpdating: isUpdating,
startUpdating: startUpdating,
stopUpdating: stopUpdating
});
}
if (kickButton || banButton || muteButton || redactButton || children) {
return /*#__PURE__*/_react.default.createElement(Container, null, muteButton, redactButton, kickButton, banButton, children);
}
return /*#__PURE__*/_react.default.createElement("div", null);
};
exports.RoomAdminToolsContainer = RoomAdminToolsContainer;
const useIsSynapseAdmin = cli => {
return (0, _useAsyncMemo.useAsyncMemo)(async () => cli ? cli.isSynapseAdministrator().catch(() => false) : false, [cli], false);
};
const useHomeserverSupportsCrossSigning = cli => {
return (0, _useAsyncMemo.useAsyncMemo)(async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
}, [cli], false);
};
function useRoomPermissions(cli, room, user) {
const [roomPermissions, setRoomPermissions] = (0, _react.useState)({
// 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 = (0, _react.useCallback)(() => {
const powerLevels = room?.currentState.getStateEvents(_matrix.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?.[_matrix.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]);
(0, _useEventEmitter.useTypedEventEmitter)(cli, _matrix.RoomStateEvent.Update, updateRoomPermissions);
(0, _react.useEffect)(() => {
updateRoomPermissions();
return () => {
setRoomPermissions({
modifyLevelMax: -1,
canEdit: false,
canInvite: false
});
};
}, [updateRoomPermissions]);
return roomPermissions;
}
const PowerLevelSection = ({
user,
room,
roomPermissions,
powerLevels
}) => {
if (roomPermissions.canEdit) {
return /*#__PURE__*/_react.default.createElement(PowerLevelEditor, {
user: user,
room: room,
roomPermissions: roomPermissions
});
} else {
const powerLevelUsersDefault = powerLevels.users_default || 0;
const powerLevel = user.powerLevel;
const role = (0, _Roles.textualPowerLevel)(powerLevel, powerLevelUsersDefault);
return /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_profileField"
}, /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_roleDescription"
}, role));
}
};
const PowerLevelEditor = ({
user,
room,
roomPermissions
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const [selectedPowerLevel, setSelectedPowerLevel] = (0, _react.useState)(user.powerLevel);
(0, _react.useEffect)(() => {
setSelectedPowerLevel(user.powerLevel);
}, [user]);
const onPowerChange = (0, _react.useCallback)(async powerLevel => {
setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId, target, powerLevel) => {
return cli.setPowerLevel(roomId, target, powerLevel).then(function () {
// NO-OP; rely on the m.room.member event coming down else we could
// get out of sync if we force setState here!
_logger.logger.log("Power change success");
}, function (err) {
_logger.logger.error("Failed to change power level " + err);
_Modal.default.createDialog(_ErrorDialog.default, {
title: (0, _languageHandler._t)("common|error"),
description: (0, _languageHandler._t)("error|update_power_level")
});
});
};
const roomId = user.roomId;
const target = user.userId;
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
const myUserId = cli.getUserId();
const myPower = powerLevelEvent.getContent().users[myUserId || ""];
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
const {
finished
} = _Modal.default.createDialog(_QuestionDialog.default, {
title: (0, _languageHandler._t)("common|warning"),
description: /*#__PURE__*/_react.default.createElement("div", null, (0, _languageHandler._t)("user_info|promote_warning"), /*#__PURE__*/_react.default.createElement("br", null), (0, _languageHandler._t)("common|are_you_sure")),
button: (0, _languageHandler._t)("action|continue")
});
const [confirmed] = await finished;
if (!confirmed) return;
} else if (myUserId === target && myPower && parseInt(myPower) > powerLevel) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
_logger.logger.error("Failed to warn about self demotion: ", e);
}
}
await applyPowerChange(roomId, target, powerLevel);
}, [user.roomId, user.userId, cli, room]);
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
return /*#__PURE__*/_react.default.createElement("div", {
className: "mx_UserInfo_profileField"
}, /*#__PURE__*/_react.default.createElement(_PowerSelector.default, {
label: undefined,
value: selectedPowerLevel,
maxValue: roomPermissions.modifyLevelMax,
usersDefault: powerLevelUsersDefault,
onChange: onPowerChange
}));
};
exports.PowerLevelEditor = PowerLevelEditor;
async function getUserDeviceInfo(userId, cli, downloadUncached = false) {
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], downloadUncached);
const devicesMap = userDeviceMap?.get(userId);
if (!devicesMap) return;
return Array.from(devicesMap.values());
}
const useDevices = userId => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
const [devices, setDevices] = (0, _react.useState)(undefined);
// Download device lists
(0, _react.useEffect)(() => {
setDevices(undefined);
let cancelled = false;
async function downloadDeviceList() {
try {
const devices = await getUserDeviceInfo(userId, cli, true);
if (cancelled || !devices) {
// we got cancelled - presumably a different user now
return;
}
disambiguateDevices(devices);
setDevices(devices);
} catch (err) {
setDevices(null);
}
}
downloadDeviceList();
// Handle being unmounted
return () => {
cancelled = true;
};
}, [cli, userId]);
// Listen to changes
(0, _react.useEffect)(() => {
let cancel = false;
const updateDevices = async () => {
const newDevices = await getUserDeviceInfo(userId, cli);
if (cancel || !newDevices) return;
setDevices(newDevices);
};
const onDevicesUpdated = users => {
if (!users.includes(userId)) return;
updateDevices();
};
const onUserTrustStatusChanged = (_userId, trustLevel) => {
if (_userId !== userId) return;
updateDevices();
};
cli.on(_crypto.CryptoEvent.DevicesUpdated, onDevicesUpdated);
cli.on(_crypto.CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener(_crypto.CryptoEvent.DevicesUpdated, onDevicesUpdated);
cli.removeListener(_crypto.CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
};
}, [cli, userId]);
return devices;
};
exports.useDevices = useDevices;
const BasicUserInfo = ({
room,
member,
devices,
isRoomEncrypted
}) => {
const cli = (0, _react.useContext)(_MatrixClientContext.default);
const powerLevels = use