UNPKG

matrix-react-sdk

Version:
1,117 lines (1,081 loc) 213 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; 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 _lodash = require("lodash"); var _icons = require("@vector-im/compound-design-tokens/assets/web/icons"); var _iconEmailPillAvatar = require("../../../../res/img/icon-email-pill-avatar.svg"); var _languageHandler = require("../../../languageHandler"); var _MatrixClientPeg = require("../../../MatrixClientPeg"); var _Permalinks = require("../../../utils/permalinks/Permalinks"); var _DMRoomMap = _interopRequireDefault(require("../../../utils/DMRoomMap")); var Email = _interopRequireWildcard(require("../../../email")); var _IdentityServerUtils = require("../../../utils/IdentityServerUtils"); var _SortMembers = require("../../../utils/SortMembers"); var _UrlUtils = require("../../../utils/UrlUtils"); var _IdentityAuthClient = _interopRequireDefault(require("../../../IdentityAuthClient")); var _humanize = require("../../../utils/humanize"); var _RoomInvite = require("../../../RoomInvite"); var _actions = require("../../../dispatcher/actions"); var _models = require("../../../stores/room-list/models"); var _RoomListStore = _interopRequireDefault(require("../../../stores/room-list/RoomListStore")); var _SettingsStore = _interopRequireDefault(require("../../../settings/SettingsStore")); var _UIFeature = require("../../../settings/UIFeature"); var _Media = require("../../../customisations/Media"); var _BaseAvatar = _interopRequireDefault(require("../avatars/BaseAvatar")); var _SearchResultAvatar = require("../avatars/SearchResultAvatar"); var _AccessibleButton = _interopRequireDefault(require("../elements/AccessibleButton")); var _strings = require("../../../utils/strings"); var _Field = _interopRequireDefault(require("../elements/Field")); var _TabbedView = _interopRequireWildcard(require("../../structures/TabbedView")); var _DialPad = _interopRequireDefault(require("../voip/DialPad")); var _QuestionDialog = _interopRequireDefault(require("./QuestionDialog")); var _Spinner = _interopRequireDefault(require("../elements/Spinner")); var _BaseDialog = _interopRequireDefault(require("./BaseDialog")); var _DialPadBackspaceButton = _interopRequireDefault(require("../elements/DialPadBackspaceButton")); var _LegacyCallHandler = _interopRequireDefault(require("../../../LegacyCallHandler")); var _UserIdentifier = _interopRequireDefault(require("../../../customisations/UserIdentifier")); var _CopyableText = _interopRequireDefault(require("../elements/CopyableText")); var _KeyboardShortcuts = require("../../../accessibility/KeyboardShortcuts"); var _KeyBindingsManager = require("../../../KeyBindingsManager"); var _directMessages = require("../../../utils/direct-messages"); var _InviteDialogTypes = require("./InviteDialogTypes"); var _Modal = _interopRequireDefault(require("../../../Modal")); var _dispatcher = _interopRequireDefault(require("../../../dispatcher/dispatcher")); var _rooms = require("../../../utils/rooms"); var _MultiInviter = require("../../../utils/MultiInviter"); var _AskInviteAnywayDialog = _interopRequireDefault(require("./AskInviteAnywayDialog")); var _SDKContext = require("../../../contexts/SDKContext"); 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; } /* Copyright 2024 New Vector Ltd. Copyright 2019-2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ const extractTargetUnknownProfiles = async (targets, profilesStores) => { const directoryMembers = targets.filter(t => t instanceof _directMessages.DirectoryMember); await Promise.all(directoryMembers.map(t => profilesStores.getOrFetchProfile(t.userId))); return directoryMembers.reduce((unknownProfiles, target) => { const lookupError = profilesStores.getProfileLookupError(target.userId); if (lookupError instanceof _matrix.MatrixError && lookupError.errcode && _MultiInviter.UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode)) { unknownProfiles.push({ userId: target.userId, errorText: lookupError.data.error || "" }); } return unknownProfiles; }, []); }; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked var TabId = /*#__PURE__*/function (TabId) { TabId["UserDirectory"] = "users"; TabId["DialPad"] = "dialpad"; return TabId; }(TabId || {}); class DMUserTile extends _react.default.PureComponent { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "onRemove", e => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); this.props.onRemove(this.props.member); }); } render() { const avatarSize = "20px"; const avatar = /*#__PURE__*/_react.default.createElement(_SearchResultAvatar.SearchResultAvatar, { user: this.props.member, size: avatarSize }); let closeButton; if (this.props.onRemove) { closeButton = /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { className: "mx_InviteDialog_userTile_remove", onClick: this.onRemove, "aria-label": (0, _languageHandler._t)("action|remove") }, /*#__PURE__*/_react.default.createElement(_icons.CloseIcon, { width: "16px", height: "16px" })); } return /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_userTile" }, /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_userTile_pill" }, avatar, /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_userTile_name" }, this.props.member.name)), closeButton); } } /** * Converts a RoomMember to a Member. * Returns the Member if it is already a Member. */ const toMember = member => { return member instanceof _matrix.RoomMember ? new _directMessages.DirectoryMember({ user_id: member.userId, display_name: member.name, avatar_url: member.getMxcAvatarUrl() }) : member; }; class DMRoomTile extends _react.default.PureComponent { constructor(...args) { super(...args); (0, _defineProperty2.default)(this, "onClick", e => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); this.props.onToggle(this.props.member); }); } highlightName(str) { if (!this.props.highlightWord) return str; // We convert things to lowercase for index searching, but pull substrings from // the submitted text to preserve case. Note: we don't need to htmlEntities the // string because React will safely encode the text for us. const lowerStr = str.toLowerCase(); const filterStr = this.props.highlightWord.toLowerCase(); const result = []; let i = 0; let ii; while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) { // Push any text we missed (first bit/middle of text) if (ii > i) { // Push any text we aren't highlighting (middle of text match, or beginning of text) result.push( /*#__PURE__*/_react.default.createElement("span", { key: i + "begin" }, str.substring(i, ii))); } i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching) // Highlight the word the user entered const substr = str.substring(i, filterStr.length + i); result.push( /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_tile--room_highlight", key: i + "bold" }, substr)); i += substr.length; } // Push any text we missed (end of text) if (i < str.length) { result.push( /*#__PURE__*/_react.default.createElement("span", { key: i + "end" }, str.substring(i))); } return result; } render() { let timestamp; if (this.props.lastActiveTs) { const humanTs = (0, _humanize.humanizeTime)(this.props.lastActiveTs); timestamp = /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_tile--room_time" }, humanTs); } const avatarSize = "36px"; const avatar = this.props.member.isEmail ? /*#__PURE__*/_react.default.createElement(_iconEmailPillAvatar.Icon, { width: avatarSize, height: avatarSize }) : /*#__PURE__*/_react.default.createElement(_BaseAvatar.default, { url: this.props.member.getMxcAvatarUrl() ? (0, _Media.mediaFromMxc)(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(parseInt(avatarSize, 10)) : null, name: this.props.member.name, idName: this.props.member.userId, size: avatarSize }); let checkmark; if (this.props.isSelected) { // To reduce flickering we put the 'selected' room tile above the real avatar checkmark = /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_tile--room_selected" }); } // To reduce flickering we put the checkmark on top of the actual avatar (prevents // the browser from reloading the image source when the avatar remounts). const stackedAvatar = /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_tile_avatarStack" }, avatar, checkmark); const userIdentifier = _UserIdentifier.default.getDisplayUserIdentifier(this.props.member.userId, { withDisplayName: true }); const caption = this.props.member.isEmail ? (0, _languageHandler._t)("invite|email_caption") : this.highlightName(userIdentifier || this.props.member.userId); return /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { className: "mx_InviteDialog_tile mx_InviteDialog_tile--room", onClick: this.onClick }, stackedAvatar, /*#__PURE__*/_react.default.createElement("span", { className: "mx_InviteDialog_tile_nameStack" }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_tile_nameStack_name" }, this.highlightName(this.props.member.name)), /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_tile_nameStack_userId" }, caption)), timestamp); } } function isRoomInvite(props) { return props.kind === _InviteDialogTypes.InviteKind.Invite; } class InviteDialog extends _react.default.PureComponent { constructor(props) { super(props); (0, _defineProperty2.default)(this, "debounceTimer", null); // actually number because we're in the browser (0, _defineProperty2.default)(this, "editorRef", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "numberEntryFieldRef", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "unmounted", false); (0, _defineProperty2.default)(this, "encryptionByDefault", false); (0, _defineProperty2.default)(this, "profilesStore", void 0); (0, _defineProperty2.default)(this, "onConsultFirstChange", ev => { this.setState({ consultFirst: ev.target.checked }); }); /** * Check if there are unknown profiles if promptBeforeInviteUnknownUsers setting is enabled. * If so show the "invite anyway?" dialog. Otherwise directly create the DM local room. */ (0, _defineProperty2.default)(this, "checkProfileAndStartDm", async () => { this.setBusy(true); const targets = this.convertFilter(); if (_SettingsStore.default.getValue("promptBeforeInviteUnknownUsers")) { const unknownProfileUsers = await extractTargetUnknownProfiles(targets, this.profilesStore); if (unknownProfileUsers.length) { this.showAskInviteAnywayDialog(unknownProfileUsers); return; } } await this.startDm(); }); (0, _defineProperty2.default)(this, "startDm", async () => { this.setBusy(true); try { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const targets = this.convertFilter(); await (0, _directMessages.startDmOnFirstMessage)(cli, targets); this.props.onFinished(true); } catch (err) { _logger.logger.error(err); this.setState({ busy: false, errorText: (0, _languageHandler._t)("invite|error_dm") }); } }); (0, _defineProperty2.default)(this, "inviteUsers", async () => { if (this.props.kind !== _InviteDialogTypes.InviteKind.Invite) return; this.setState({ busy: true }); this.convertFilter(); const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const room = cli.getRoom(this.props.roomId); if (!room) { _logger.logger.error("Failed to find the room to invite users to"); this.setState({ busy: false, errorText: (0, _languageHandler._t)("invite|error_find_room") }); return; } try { const result = await (0, _RoomInvite.inviteMultipleToRoom)(cli, this.props.roomId, targetIds); if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too this.props.onFinished(true); } } catch (err) { _logger.logger.error(err); this.setState({ busy: false, errorText: (0, _languageHandler._t)("invite|error_invite") }); } }); (0, _defineProperty2.default)(this, "transferCall", async () => { if (this.props.kind !== _InviteDialogTypes.InviteKind.CallTransfer) return; if (this.state.currentTabId == TabId.UserDirectory) { this.convertFilter(); const targets = this.convertFilter(); const targetIds = targets.map(t => t.userId); if (targetIds.length > 1) { this.setState({ errorText: (0, _languageHandler._t)("invite|error_transfer_multiple_target") }); return; } _LegacyCallHandler.default.instance.startTransferToMatrixID(this.props.call, targetIds[0], this.state.consultFirst); } else { _LegacyCallHandler.default.instance.startTransferToPhoneNumber(this.props.call, this.state.dialPadValue, this.state.consultFirst); } this.props.onFinished(true); }); (0, _defineProperty2.default)(this, "onKeyDown", e => { if (this.state.busy) return; let handled = false; const value = e.currentTarget.value.trim(); const action = (0, _KeyBindingsManager.getKeyBindingsManager)().getAccessibilityAction(e); switch (action) { case _KeyboardShortcuts.KeyBindingAction.Backspace: if (value || this.state.targets.length <= 0) break; // when the field is empty and the user hits backspace remove the right-most target this.removeMember(this.state.targets[this.state.targets.length - 1]); handled = true; break; case _KeyboardShortcuts.KeyBindingAction.Space: if (!value || !value.includes("@") || value.includes(" ")) break; // when the user hits space and their input looks like an e-mail/MXID then try to convert it this.convertFilter(); handled = true; break; case _KeyboardShortcuts.KeyBindingAction.Enter: if (!value) break; // when the user hits enter with something in their field try to convert it this.convertFilter(); handled = true; break; } if (handled) { e.preventDefault(); } }); (0, _defineProperty2.default)(this, "onCancel", () => { this.props.onFinished(false); }); (0, _defineProperty2.default)(this, "updateSuggestions", async term => { _MatrixClientPeg.MatrixClientPeg.safeGet().searchUserDirectory({ term }).then(async r => { if (term !== this.state.filterText) { // Discard the results - we were probably too slow on the server-side to make // these results useful. This is a race we want to avoid because we could overwrite // more accurate results. return; } if (!r.results) r.results = []; // While we're here, try and autocomplete a search result for the mxid itself // if there's no matches (and the input looks like a mxid). if (term[0] === "@" && term.indexOf(":") > 1) { try { const profile = await this.profilesStore.getOrFetchProfile(term, { shouldThrow: true }); if (profile) { // If we have a profile, we have enough information to assume that // the mxid can be invited - add it to the list. We stick it at the // top so it is most obviously presented to the user. r.results.splice(0, 0, { user_id: term, display_name: profile["displayname"], avatar_url: profile["avatar_url"] }); } } catch (e) { _logger.logger.warn("Non-fatal error trying to make an invite for a user ID", e); } } this.setState({ serverResultsMixin: r.results.map(u => ({ userId: u.user_id, user: new _directMessages.DirectoryMember(u) })) }); }).catch(e => { _logger.logger.error("Error searching user directory:"); _logger.logger.error(e); this.setState({ serverResultsMixin: [] }); // clear results because it's moderately fatal }); // Whenever we search the directory, also try to search the identity server. It's // all debounced the same anyways. if (!this.state.canUseIdentityServer) { // The user doesn't have an identity server set - warn them of that. this.setState({ tryingIdentityServer: true }); return; } if (Email.looksValid(term) && this.canInviteThirdParty() && _SettingsStore.default.getValue(_UIFeature.UIFeature.IdentityServer)) { // Start off by suggesting the plain email while we try and resolve it // to a real account. this.setState({ // per above: the userId is a lie here - it's just a regular identifier threepidResultsMixin: [{ user: new _directMessages.ThreepidMember(term), userId: term }] }); try { const authClient = new _IdentityAuthClient.default(); const token = await authClient.getAccessToken(); // No token → unable to try a lookup if (!token) return; if (term !== this.state.filterText) return; // abandon hope const lookup = await _MatrixClientPeg.MatrixClientPeg.safeGet().lookupThreePid("email", term, token); if (term !== this.state.filterText) return; // abandon hope if (!lookup || !("mxid" in lookup)) { // We weren't able to find anyone - we're already suggesting the plain email // as an alternative, so do nothing. return; } // We append the user suggestion to give the user an option to click // the email anyways, and so we don't cause things to jump around. In // theory, the user would see the user pop up and think "ah yes, that // person!" const profile = await this.profilesStore.getOrFetchProfile(lookup.mxid); if (term !== this.state.filterText || !profile) return; // abandon hope this.setState({ threepidResultsMixin: [...this.state.threepidResultsMixin, { user: new _directMessages.DirectoryMember({ user_id: lookup.mxid, display_name: profile.displayname, avatar_url: profile.avatar_url }), // Use the search term as identifier, so that it shows up in suggestions. userId: term }] }); } catch (e) { _logger.logger.error("Error searching identity server:"); _logger.logger.error(e); this.setState({ threepidResultsMixin: [] }); // clear results because it's moderately fatal } } }); (0, _defineProperty2.default)(this, "updateFilter", e => { const term = e.target.value; this.setState({ filterText: term }); // Debounce server lookups to reduce spam. We don't clear the existing server // results because they might still be vaguely accurate, likewise for races which // could happen here. if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = window.setTimeout(() => { this.updateSuggestions(term); }, 150); // 150ms debounce (human reaction time + some) }); (0, _defineProperty2.default)(this, "showMoreRecents", () => { this.setState({ numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN }); }); (0, _defineProperty2.default)(this, "showMoreSuggestions", () => { this.setState({ numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN }); }); (0, _defineProperty2.default)(this, "toggleMember", member => { if (!this.state.busy) { let filterText = this.state.filterText; let targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.findIndex(m => m.userId === member.userId); if (idx >= 0) { targets.splice(idx, 1); } else { if (this.props.kind === _InviteDialogTypes.InviteKind.CallTransfer && targets.length > 0) { targets = []; } targets.push(member); filterText = ""; // clear the filter when the user accepts a suggestion } this.setState({ targets, filterText }); if (this.editorRef && this.editorRef.current) { this.editorRef.current.focus(); } } }); (0, _defineProperty2.default)(this, "removeMember", member => { const targets = this.state.targets.map(t => t); // cheap clone for mutation const idx = targets.indexOf(member); if (idx >= 0) { targets.splice(idx, 1); this.setState({ targets }); } if (this.editorRef && this.editorRef.current) { this.editorRef.current.focus(); } }); (0, _defineProperty2.default)(this, "onPaste", async e => { if (this.state.filterText) { // if the user has already typed something, just let them // paste normally. return; } const text = e.clipboardData.getData("text"); const potentialAddresses = this.parseFilter(text); // one search term which is not a mxid or email address if (potentialAddresses.length === 1 && !potentialAddresses[0].includes("@")) { return; } // Prevent the text being pasted into the input e.preventDefault(); // Process it as a list of addresses to add instead const possibleMembers = [ // If we can avoid hitting the profile endpoint, we should. ...this.state.recents, ...this.state.suggestions, ...this.state.serverResultsMixin, ...this.state.threepidResultsMixin]; const toAdd = []; const failed = []; // Addresses that could not be added. // Will be displayed as filter text to provide feedback. const unableToAddMore = []; for (const address of potentialAddresses) { const member = possibleMembers.find(m => m.userId === address); if (member) { if (this.canInviteMore([...this.state.targets, ...toAdd])) { toAdd.push(member.user); } else { // Invite not possible for current targets and pasted targets. unableToAddMore.push(address); } continue; } if (Email.looksValid(address)) { if (this.canInviteThirdParty([...this.state.targets, ...toAdd])) { toAdd.push(new _directMessages.ThreepidMember(address)); } else { // Third-party invite not possible for current targets and pasted targets. unableToAddMore.push(address); } continue; } if (address[0] !== "@") { failed.push(address); // not a user ID continue; } try { const profile = await this.profilesStore.getOrFetchProfile(address); toAdd.push(new _directMessages.DirectoryMember({ user_id: address, display_name: profile?.displayname, avatar_url: profile?.avatar_url })); } catch (e) { _logger.logger.error("Error looking up profile for " + address); _logger.logger.error(e); failed.push(address); } } if (this.unmounted) return; if (failed.length > 0) { _Modal.default.createDialog(_QuestionDialog.default, { title: (0, _languageHandler._t)("invite|error_find_user_title"), description: (0, _languageHandler._t)("invite|error_find_user_description", { csvNames: failed.join(", ") }), button: (0, _languageHandler._t)("action|ok") }); } if (unableToAddMore) { this.setState({ filterText: unableToAddMore.join(" "), targets: (0, _lodash.uniqBy)([...this.state.targets, ...toAdd], t => t.userId) }); } else { this.setState({ targets: (0, _lodash.uniqBy)([...this.state.targets, ...toAdd], t => t.userId) }); } }); (0, _defineProperty2.default)(this, "onClickInputArea", e => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); if (this.editorRef && this.editorRef.current) { this.editorRef.current.focus(); } }); (0, _defineProperty2.default)(this, "onUseDefaultIdentityServerClick", e => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. // eslint-disable-next-line react-hooks/rules-of-hooks (0, _IdentityServerUtils.setToDefaultIdentityServer)(_MatrixClientPeg.MatrixClientPeg.safeGet()); this.setState({ canUseIdentityServer: true, tryingIdentityServer: false }); }); (0, _defineProperty2.default)(this, "onManageSettingsClick", e => { e.preventDefault(); _dispatcher.default.fire(_actions.Action.ViewUserSettings); this.props.onFinished(false); }); (0, _defineProperty2.default)(this, "onDialFormSubmit", ev => { ev.preventDefault(); this.transferCall(); }); (0, _defineProperty2.default)(this, "onDialChange", ev => { this.setState({ dialPadValue: ev.currentTarget.value }); }); (0, _defineProperty2.default)(this, "onDigitPress", (digit, ev) => { this.setState({ dialPadValue: this.state.dialPadValue + digit }); // Keep the number field focused so that keyboard entry is still available // However, don't focus if this wasn't the result of directly clicking on the button, // i.e someone using keyboard navigation. if (ev.type === "click") { this.numberEntryFieldRef.current?.focus(); } }); (0, _defineProperty2.default)(this, "onDeletePress", ev => { if (this.state.dialPadValue.length === 0) return; this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) }); // Keep the number field focused so that keyboard entry is still available // However, don't focus if this wasn't the result of directly clicking on the button, // i.e someone using keyboard navigation. if (ev.type === "click") { this.numberEntryFieldRef.current?.focus(); } }); (0, _defineProperty2.default)(this, "onTabChange", tabId => { this.setState({ currentTabId: tabId }); }); if (props.kind === _InviteDialogTypes.InviteKind.Invite && !props.roomId) { throw new Error("When using InviteKind.Invite a roomId is required for an InviteDialog"); } else if (props.kind === _InviteDialogTypes.InviteKind.CallTransfer && !props.call) { throw new Error("When using InviteKind.CallTransfer a call is required for an InviteDialog"); } this.profilesStore = _SDKContext.SdkContextClass.instance.userProfilesStore; const excludedIds = new Set([_MatrixClientPeg.MatrixClientPeg.safeGet().getUserId()]); if (isRoomInvite(props)) { const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(props.roomId); const isFederated = room?.currentState.getStateEvents(_matrix.EventType.RoomCreate, "")?.getContent()["m.federate"]; if (!room) throw new Error("Room ID given to InviteDialog does not look like a room"); room.getMembersWithMembership(_types.KnownMembership.Invite).forEach(m => excludedIds.add(m.userId)); room.getMembersWithMembership(_types.KnownMembership.Join).forEach(m => excludedIds.add(m.userId)); // add banned users, so we don't try to invite them room.getMembersWithMembership(_types.KnownMembership.Ban).forEach(m => excludedIds.add(m.userId)); if (isFederated === false) { // exclude users from external servers const homeserver = props.roomId.split(":")[1]; this.excludeExternals(homeserver, excludedIds); } } this.state = { targets: [], // array of Member objects (see interface above) filterText: this.props.initialText || "", // Mutates alreadyInvited set so that buildSuggestions doesn't duplicate any users recents: InviteDialog.buildRecents(excludedIds), numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this.buildSuggestions(excludedIds), numSuggestionsShown: INITIAL_ROOMS_SHOWN, serverResultsMixin: [], threepidResultsMixin: [], canUseIdentityServer: !!_MatrixClientPeg.MatrixClientPeg.safeGet().getIdentityServerUrl(), tryingIdentityServer: false, consultFirst: false, dialPadValue: "", currentTabId: TabId.UserDirectory, // These two flags are used for the 'Go' button to communicate what is going on. busy: false }; } componentDidMount() { this.encryptionByDefault = (0, _rooms.privateShouldBeEncrypted)(_MatrixClientPeg.MatrixClientPeg.safeGet()); if (this.props.initialText) { this.updateSuggestions(this.props.initialText); } } componentWillUnmount() { this.unmounted = true; } excludeExternals(homeserver, excludedTargetIds) { const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); // users with room membership const members = Object.values((0, _SortMembers.buildMemberScores)(client)).map(({ member }) => member.userId); // users with dm membership const roomMembers = Object.keys(_DMRoomMap.default.shared().getUniqueRoomsWithIndividuals()); roomMembers.forEach(id => members.push(id)); // filter duplicates and user IDs from external servers const externals = new Set(members.filter(id => !id.includes(homeserver))); externals.forEach(id => excludedTargetIds.add(id)); } static buildRecents(excludedTargetIds) { const rooms = _DMRoomMap.default.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. const dmTaggedRooms = _RoomListStore.default.instance.orderedLists[_models.DefaultTagID.DM] || []; const myUserId = _MatrixClientPeg.MatrixClientPeg.safeGet().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); for (const member of otherMembers) { if (rooms[member.userId]) continue; // already have a room _logger.logger.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`); rooms[member.userId] = dmRoom; } } const recents = []; for (const userId in rooms) { // Filter out user IDs that are already in the room / should be excluded if (excludedTargetIds.has(userId)) { _logger.logger.warn(`[Invite:Recents] Excluding ${userId} from recents`); continue; } const room = rooms[userId]; const roomMember = room.getMember(userId); if (!roomMember) { // just skip people who don't have memberships for some reason _logger.logger.warn(`[Invite:Recents] ${userId} is missing a member object in their own DM (${room.roomId})`); continue; } // Find the last timestamp for a message event const searchTypes = ["m.room.message", "m.room.encrypted", "m.sticker"]; const maxSearchEvents = 20; // to prevent traversing history let lastEventTs = 0; if (room.timeline && room.timeline.length) { for (let i = room.timeline.length - 1; i >= 0; i--) { const ev = room.timeline[i]; if (searchTypes.includes(ev.getType())) { lastEventTs = ev.getTs(); break; } if (room.timeline.length - i > maxSearchEvents) break; } } if (!lastEventTs) { // something weird is going on with this room _logger.logger.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`); continue; } recents.push({ userId, user: toMember(roomMember), lastActive: lastEventTs }); // We mutate the given set so that any later callers avoid duplicating these users excludedTargetIds.add(userId); } if (!recents) _logger.logger.warn("[Invite:Recents] No recents to suggest!"); // Sort the recents by last active to save us time later recents.sort((a, b) => b.lastActive - a.lastActive); return recents; } buildSuggestions(excludedTargetIds) { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const activityScores = (0, _SortMembers.buildActivityScores)(cli); const memberScores = (0, _SortMembers.buildMemberScores)(cli); const memberComparator = (0, _SortMembers.compareMembers)(activityScores, memberScores); return Object.values(memberScores).map(({ member }) => member).filter(member => !excludedTargetIds.has(member.userId)).sort(memberComparator).map(member => ({ userId: member.userId, user: toMember(member) })); } shouldAbortAfterInviteError(result, room) { this.setState({ busy: false }); const userMap = new Map(this.state.targets.map(member => [member.userId, member])); return !(0, _RoomInvite.showAnyInviteErrors)(result.states, room, result.inviter, userMap); } convertFilter() { // Check to see if there's anything to convert first if (!this.state.filterText || !this.state.filterText.includes("@")) return this.state.targets || []; if (!this.canInviteMore()) { // There should only be one third-party invite → do not allow more targets return this.state.targets; } let newMember; if (this.state.filterText.startsWith("@")) { // Assume mxid newMember = new _directMessages.DirectoryMember({ user_id: this.state.filterText }); } else if (_SettingsStore.default.getValue(_UIFeature.UIFeature.IdentityServer)) { // Assume email if (this.canInviteThirdParty()) { newMember = new _directMessages.ThreepidMember(this.state.filterText); } } if (!newMember) return this.state.targets; const newTargets = [...(this.state.targets || []), newMember]; this.setState({ targets: newTargets, filterText: "" }); return newTargets; } setBusy(busy) { this.setState({ busy }); } showAskInviteAnywayDialog(unknownProfileUsers) { _Modal.default.createDialog(_AskInviteAnywayDialog.default, { unknownProfileUsers, onInviteAnyways: () => this.startDm(), onGiveUp: () => { this.setBusy(false); }, description: (0, _languageHandler._t)("invite|ask_anyway_description"), inviteNeverWarnLabel: (0, _languageHandler._t)("invite|ask_anyway_never_warn_label"), inviteLabel: (0, _languageHandler._t)("invite|ask_anyway_label") }); } parseFilter(filter) { return filter.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings } renderSection(kind) { let sourceMembers = kind === "recents" ? this.state.recents : this.state.suggestions; let showNum = kind === "recents" ? this.state.numRecentsShown : this.state.numSuggestionsShown; const showMoreFn = kind === "recents" ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this); const lastActive = m => kind === "recents" ? m.lastActive : undefined; let sectionName = kind === "recents" ? (0, _languageHandler._t)("invite|recents_section") : (0, _languageHandler._t)("common|suggestions"); if (this.props.kind === _InviteDialogTypes.InviteKind.Invite) { sectionName = kind === "recents" ? (0, _languageHandler._t)("invite|suggestions_section") : (0, _languageHandler._t)("common|suggestions"); } // Mix in the server results if we have any, but only if we're searching. We track the additional // members separately because we want to filter sourceMembers but trust the mixin arrays to have // the right members in them. let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin; if (this.state.filterText && hasMixins && kind === "suggestions") { // We don't want to duplicate members though, so just exclude anyone we've already seen. // The type of u is a pain to define but members of both mixins have the 'userId' property const notAlreadyExists = u => { return !this.state.recents.some(m => m.userId === u.userId) && !sourceMembers.some(m => m.userId === u.userId) && !priorityAdditionalMembers.some(m => m.userId === u.userId) && !otherAdditionalMembers.some(m => m.userId === u.userId); }; otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists); priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists); } const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0; // Hide the section if there's nothing to filter by if (sourceMembers.length === 0 && !hasAdditionalMembers) return null; if (!this.canInviteThirdParty()) { // It is currently not allowed to add more third-party invites. Filter them out. priorityAdditionalMembers = priorityAdditionalMembers.filter(s => s instanceof _directMessages.ThreepidMember); } // Do some simple filtering on the input before going much further. If we get no results, say so. if (this.state.filterText) { const filterBy = this.state.filterText.toLowerCase(); sourceMembers = sourceMembers.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy)); if (sourceMembers.length === 0 && !hasAdditionalMembers) { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_section" }, /*#__PURE__*/_react.default.createElement("h3", null, sectionName), /*#__PURE__*/_react.default.createElement("p", null, (0, _languageHandler._t)("common|no_results"))); } } // Now we mix in the additional members. Again, we presume these have already been filtered. We // also assume they are more relevant than our suggestions and prepend them to the list. sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers]; // If we're going to hide one member behind 'show more', just use up the space of the button // with the member's tile instead. if (showNum === sourceMembers.length - 1) showNum++; // .slice() will return an incomplete array but won't error on us if we go too far const toRender = sourceMembers.slice(0, showNum); const hasMore = toRender.length < sourceMembers.length; let showMore; if (hasMore) { showMore = /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_section_showMore" }, /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { onClick: showMoreFn, kind: "link" }, (0, _languageHandler._t)("common|show_more"))); } const tiles = toRender.map(r => /*#__PURE__*/_react.default.createElement(DMRoomTile, { member: r.user, lastActiveTs: lastActive(r), key: r.user.userId, onToggle: this.toggleMember, highlightWord: this.state.filterText, isSelected: this.state.targets.some(t => t.userId === r.userId) })); return /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_section" }, /*#__PURE__*/_react.default.createElement("h3", null, sectionName), tiles, showMore); } renderEditor() { const hasPlaceholder = this.props.kind == _InviteDialogTypes.InviteKind.CallTransfer && this.state.targets.length === 0 && this.state.filterText.length === 0; const targets = this.state.targets.map(t => /*#__PURE__*/_react.default.createElement(DMUserTile, { member: t, onRemove: this.state.busy ? undefined : this.removeMember, key: t.userId })); const input = /*#__PURE__*/_react.default.createElement("input", { type: "text", onKeyDown: this.onKeyDown, onChange: this.updateFilter, value: this.state.filterText, ref: this.editorRef, onPaste: this.onPaste, autoFocus: true, disabled: this.state.busy || this.props.kind == _InviteDialogTypes.InviteKind.CallTransfer && this.state.targets.length > 0, autoComplete: "off", placeholder: hasPlaceholder ? (0, _languageHandler._t)("action|search") : undefined, "data-testid": "invite-dialog-input" }); return /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_editor", onClick: this.onClickInputArea }, targets, input); } renderIdentityServerWarning() { if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer || !_SettingsStore.default.getValue(_UIFeature.UIFeature.IdentityServer)) { return null; } const defaultIdentityServerUrl = (0, _IdentityServerUtils.getDefaultIdentityServerUrl)(); if (defaultIdentityServerUrl) { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_identityServer" }, (0, _languageHandler._t)("invite|email_use_default_is", { defaultIdentityServerName: (0, _UrlUtils.abbreviateUrl)(defaultIdentityServerUrl) }, { default: sub => /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { kind: "link_inline", onClick: this.onUseDefaultIdentityServerClick }, sub), settings: sub => /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { kind: "link_inline", onClick: this.onManageSettingsClick }, sub) })); } else { return /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_identityServer" }, (0, _languageHandler._t)("invite|email_use_is", {}, { settings: sub => /*#__PURE__*/_react.default.createElement(_AccessibleButton.default, { kind: "link_inline", onClick: this.onManageSettingsClick }, sub) })); } } async onLinkClick(e) { e.preventDefault(); (0, _strings.selectText)(e.currentTarget); } get screenName() { switch (this.props.kind) { case _InviteDialogTypes.InviteKind.Dm: return "StartChat"; default: return undefined; } } /** * If encryption by default is enabled, third-party invites should be encrypted as well. * For encryption to work, the other side requires a device. * To achieve this Element implements a waiting room until all have joined. * Waiting for many users degrades the UX → only one email invite is allowed at a time. * * @param targets - Optional member list to check. Uses targets from state if not provided. */ canInviteMore(targets) { targets = targets || this.state.targets; return this.canInviteThirdParty(targets) || !targets.some(t => t instanceof _directMessages.ThreepidMember); } /** * A third-party invite is possible if * - this is a non-DM dialog or * - there are no invites yet or * - encryption by default is not enabled * * Also see {@link InviteDialog#canInviteMore}. * * @param targets - Optional member list to check. Uses targets from state if not provided. */ canInviteThirdParty(targets) { targets = targets || this.state.targets; return this.props.kind !== _InviteDialogTypes.InviteKind.Dm || targets.length === 0 || !this.encryptionByDefault; } hasFilterAtLeastOneEmail() { if (!this.state.filterText) return false; return this.parseFilter(this.state.filterText).some(address => { return Email.looksValid(address); }); } render() { let spinner; if (this.state.busy) { spinner = /*#__PURE__*/_react.default.createElement(_Spinner.default, { w: 20, h: 20 }); } let title; let helpText; let buttonText; let goButtonFn = null; let consultConnectSection; let extraSection; let footer; const identityServersEnabled = _SettingsStore.default.getValue(_UIFeature.UIFeature.IdentityServer); const hasSelection = this.state.targets.length > 0 || this.state.filterText && this.state.filterText.includes("@"); const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const userId = cli.getUserId(); if (this.props.kind === _InviteDialogTypes.InviteKind.Dm) { title = (0, _languageHandler._t)("space|add_existing_room_space|dm_heading"); if (identityServersEnabled) { helpText = (0, _languageHandler._t)("invite|start_conversation_name_email_mxid_prompt", {}, { userId: () => { return /*#__PURE__*/_react.default.createElement("a", { href: (0, _Permalinks.makeUserPermalink)(userId), rel: "noreferrer noopener", target: "_blank" }, userId); } }); } else { helpText = (0, _languageHandler._t)("invite|start_conversation_name_mxid_prompt", {}, { userId: () => { return /*#__PURE__*/_react.default.createElement("a", { href: (0, _Permalinks.makeUserPermalink)(userId), rel: "noreferrer noopener", target: "_blank" }, userId); } }); } buttonText = (0, _languageHandler._t)("action|go"); goButtonFn = this.checkProfileAndStartDm; extraSection = /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_section_hidden_suggestions_disclaimer" }, /*#__PURE__*/_react.default.createElement("span", null, (0, _languageHandler._t)("invite|suggestions_disclaimer")), /*#__PURE__*/_react.default.createElement("p", null, (0, _languageHandler._t)("invite|suggestions_disclaimer_prompt"))); const link = (0, _Permalinks.makeUserPermalink)(_MatrixClientPeg.MatrixClientPeg.safeGet().getSafeUserId()); footer = /*#__PURE__*/_react.default.createElement("div", { className: "mx_InviteDialog_footer" }, /*#__PURE__*/_react.default.createElement("h3", null, (0, _languageHandler._t)("invite|send_link_prompt")), /*#__PURE__*/_react.default.createElement(_CopyableText.default, { getTextToCopy: () => (0, _Permalinks.makeUserPermalink)(_MatrixClientPeg.MatrixClientPeg.safeGet().getSafeUserId()) }, /*#__PURE__*/_react.default.createElement("a", { className: "mx_InviteDialog_footer_link", href: link, onClick: this.onLinkClick }, link))); } else if (this.props.kind === _InviteDialogTypes.InviteKind.Invite) { const roomId = this.props.roomId; const room