matrix-react-sdk
Version:
SDK for matrix.org using React
1,117 lines (1,081 loc) • 213 kB
JavaScript
"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