UNPKG

matrix-react-sdk

Version:
1,146 lines (1,130 loc) 238 kB
"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