Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Tulir Asokan 2021-03-26 21:31:28 +02:00
commit c31967dc97
111 changed files with 6271 additions and 2212 deletions

View file

@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import FormButton from "../elements/FormButton";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore";
@ -69,6 +68,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const existingRoomsSet = new Set(existingRooms);
const rooms = cli.getVisibleRooms().filter(room => {
return !existingRoomsSet.has(room) // not already in space
&& !room.isSpaceRoom() // not a space itself
&& room.name.toLowerCase().includes(lcQuery) // contains query
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
});
@ -109,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing spaces/rooms") }</h1>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
@ -127,29 +127,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section">
<h3>{ _t("Rooms") }</h3>
@ -171,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ _t("No results") }
</span> : undefined }
@ -184,8 +185,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</AccessibleButton>
</span>
<FormButton
label={busy ? _t("Applying...") : _t("Apply")}
<AccessibleButton
kind="primary"
disabled={busy || selectedToAdd.size < 1}
onClick={async () => {
setBusy(true);
@ -199,7 +200,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
}
setBusy(false);
}}
/>
>
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div>
</BaseDialog>;
};

View file

@ -43,6 +43,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import {getAddressType} from "../../../UserAddress";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -673,19 +674,20 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
console.error(err);
this.setState({
busy: false,
errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."),
errorText: _t("We couldn't create your DM."),
});
});
};
_inviteUsers = () => {
_inviteUsers = async () => {
const startTime = CountlyAnalytics.getTimestamp();
this.setState({busy: true});
this._convertFilter();
const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
if (!room) {
console.error("Failed to find the room to invite users to");
this.setState({
@ -695,12 +697,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return;
}
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => {
try {
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
this.props.onFinished();
}
}).catch(err => {
if (cli.isRoomEncrypted(this.props.roomId) &&
SettingsStore.getValue("feature_room_history_key_sharing")) {
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility == "world_readable" || visibility == "shared") {
const invitedUsers = [];
for (const [addr, state] of Object.entries(result.states)) {
if (state === "invited" && getAddressType(addr) === "mx-user-id") {
invitedUsers.push(addr);
}
}
console.log("Sharing history with", invitedUsers);
cli.sendSharedHistoryKeys(
this.props.roomId, invitedUsers,
);
}
}
} catch (err) {
console.error(err);
this.setState({
busy: false,
@ -708,7 +732,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
"We couldn't invite those users. Please check the users you want to invite and try again.",
),
});
});
}
};
_transferCall = async () => {
@ -886,19 +910,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
_toggleMember = (member: Member) => {
let filterText = this.state.filterText;
const targets = this.state.targets.map(t => t); // cheap clone for mutation
const idx = targets.indexOf(member);
if (idx >= 0) {
targets.splice(idx, 1);
} else {
targets.push(member);
filterText = ""; // clear the filter when the user accepts a suggestion
}
this.setState({targets, filterText});
if (!this.state.busy) {
let filterText = this.state.filterText;
const targets = this.state.targets.map(t => t); // cheap clone for mutation
const idx = targets.indexOf(member);
if (idx >= 0) {
targets.splice(idx, 1);
} else {
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();
if (this._editorRef && this._editorRef.current) {
this._editorRef.current.focus();
}
}
};
@ -1189,10 +1215,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
let helpText;
let buttonText;
let goButtonFn;
let keySharingWarning = <span />;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const userId = MatrixClientPeg.get().getUserId();
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
if (this.props.kind === KIND_DM) {
title = _t("Direct Messages");
@ -1288,6 +1316,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
cli.isRoomEncrypted(this.props.roomId)) {
const room = cli.getRoom(this.props.roomId);
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility === "world_readable" || visibility === "shared") {
keySharingWarning =
<p className='mx_InviteDialog_helpText'>
<img
src={require("../../../../res/img/element-icons/info.svg")}
width={14} height={14} />
{" " + _t("Invited people will be able to read old messages.")}
</p>;
}
}
} else if (this.props.kind === KIND_CALL_TRANSFER) {
title = _t("Transfer");
buttonText = _t("Transfer");
@ -1321,6 +1368,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
{spinner}
</div>
</div>
{keySharingWarning}
{this._renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div>
<div className='mx_InviteDialog_userSections'>

View file

@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import FormButton from "../elements/FormButton";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise";
@ -127,23 +126,24 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<div>
{ _t("Make this space private") }
<ToggleSwitch
checked={joinRule === "private"}
onChange={checked => setJoinRule(checked ? "private" : "invite")}
checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule}
aria-label={_t("Make this space private")}
/>
</div>
<FormButton
<AccessibleButton
kind="danger"
label={_t("Leave Space")}
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
}}
/>
>
{ _t("Leave Space") }
</AccessibleButton>
<div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") }
</AccessibleButton>
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
{ busy ? _t("Saving...") : _t("Save Changes") }
</AccessibleButton>
</div>
</div>
</BaseDialog>;

View file

@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
const member = this.props.member ||
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request");
_t("Verify other login") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
contentId="mx_Dialog_content"

View file

@ -1,6 +1,5 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -17,14 +16,15 @@ limitations under the License.
import {debounce} from "lodash";
import classNames from 'classnames';
import React from 'react';
import PropTypes from "prop-types";
import React, {ChangeEvent, FormEvent} from 'react';
import {ISecretStorageKeyInfo} from "matrix-js-sdk/src";
import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton';
import { _t } from '../../../../languageHandler';
import {_t} from '../../../../languageHandler';
import {IDialogProps} from "../IDialogProps";
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because
@ -34,22 +34,30 @@ const KEY_FILE_MAX_SIZE = 128;
// Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200;
interface IProps extends IDialogProps {
keyInfo: ISecretStorageKeyInfo;
checkPrivateKey: (k: {passphrase?: string, recoveryKey?: string}) => boolean;
}
interface IState {
recoveryKey: string;
recoveryKeyValid: boolean | null;
recoveryKeyCorrect: boolean | null;
recoveryKeyFileError: boolean | null;
forceRecoveryKey: boolean;
passPhrase: string;
keyMatches: boolean | null;
}
/*
* Access Secure Secret Storage by requesting the user's passphrase.
*/
export default class AccessSecretStorageDialog extends React.PureComponent {
static propTypes = {
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
}
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
private fileUpload = React.createRef<HTMLInputElement>();
constructor(props) {
super(props);
this._fileUpload = React.createRef();
this.state = {
recoveryKey: "",
recoveryKeyValid: null,
@ -61,21 +69,21 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
};
}
_onCancel = () => {
private onCancel = () => {
this.props.onFinished(false);
}
};
_onUseRecoveryKeyClick = () => {
private onUseRecoveryKeyClick = () => {
this.setState({
forceRecoveryKey: true,
});
}
};
_validateRecoveryKeyOnChange = debounce(() => {
this._validateRecoveryKey();
private validateRecoveryKeyOnChange = debounce(async () => {
await this.validateRecoveryKey();
}, VALIDATION_THROTTLE_MS);
async _validateRecoveryKey() {
private async validateRecoveryKey() {
if (this.state.recoveryKey === '') {
this.setState({
recoveryKeyValid: null,
@ -102,27 +110,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}
}
_onRecoveryKeyChange = (e) => {
private onRecoveryKeyChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({
recoveryKey: e.target.value,
recoveryKey: ev.target.value,
recoveryKeyFileError: null,
});
// also clear the file upload control so that the user can upload the same file
// the did before (otherwise the onchange wouldn't fire)
if (this._fileUpload.current) this._fileUpload.current.value = null;
if (this.fileUpload.current) this.fileUpload.current.value = null;
// We don't use Field's validation here because a) we want it in a separate place rather
// than in a tooltip and b) we want it to display feedback based on the uploaded file
// as well as the text box. Ideally we would refactor Field's validation logic so we could
// re-use some of it.
this._validateRecoveryKeyOnChange();
}
this.validateRecoveryKeyOnChange();
};
_onRecoveryKeyFileChange = async e => {
if (e.target.files.length === 0) return;
private onRecoveryKeyFileChange = async (ev: ChangeEvent<HTMLInputElement>) => {
if (ev.target.files.length === 0) return;
const f = e.target.files[0];
const f = ev.target.files[0];
if (f.size > KEY_FILE_MAX_SIZE) {
this.setState({
@ -140,7 +148,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
recoveryKeyFileError: null,
recoveryKey: contents.trim(),
});
this._validateRecoveryKey();
await this.validateRecoveryKey();
} else {
this.setState({
recoveryKeyFileError: true,
@ -150,14 +158,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
});
}
}
};
private onRecoveryKeyFileUploadClick = () => {
this.fileUpload.current.click();
}
_onRecoveryKeyFileUploadClick = () => {
this._fileUpload.current.click();
}
_onPassPhraseNext = async (e) => {
e.preventDefault();
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (this.state.passPhrase.length <= 0) return;
@ -169,10 +177,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else {
this.setState({ keyMatches });
}
}
};
_onRecoveryKeyNext = async (e) => {
e.preventDefault();
private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!this.state.recoveryKeyValid) return;
@ -184,16 +192,16 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else {
this.setState({ keyMatches });
}
}
};
_onPassPhraseChange = (e) => {
private onPassPhraseChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({
passPhrase: e.target.value,
passPhrase: ev.target.value,
keyMatches: null,
});
}
};
getKeyValidationText() {
private getKeyValidationText(): string {
if (this.state.recoveryKeyFileError) {
return _t("Wrong file type");
} else if (this.state.recoveryKeyCorrect) {
@ -208,7 +216,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
// Caution: Making this an import will break tests.
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const hasPassphrase = (
this.props.keyInfo &&
@ -244,18 +253,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{
button: s => <AccessibleButton className="mx_linkButton"
element="span"
onClick={this._onUseRecoveryKeyClick}
onClick={this.onUseRecoveryKeyClick}
>
{s}
</AccessibleButton>,
},
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<input
type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange}
onChange={this.onPassPhraseChange}
value={this.state.passPhrase}
autoFocus={true}
autoComplete="new-password"
@ -264,9 +273,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{keyStatus}
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNext}
onPrimaryButtonClick={this.onPassPhraseNext}
hasCancel={true}
onCancel={this._onCancel}
onCancel={this.onCancel}
focus={false}
primaryDisabled={this.state.passPhrase.length === 0}
/>
@ -291,7 +300,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<form
className="mx_AccessSecretStorageDialog_primaryContainer"
onSubmit={this._onRecoveryKeyNext}
onSubmit={this.onRecoveryKeyNext}
spellCheck={false}
autoComplete="off"
>
@ -301,7 +310,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
type="password"
label={_t('Security Key')}
value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange}
onChange={this.onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect}
autoComplete="off"
/>
@ -312,10 +321,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<div>
<input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this._fileUpload}
onChange={this._onRecoveryKeyFileChange}
ref={this.fileUpload}
onChange={this.onRecoveryKeyFileChange}
/>
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
<AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
{_t("Upload")}
</AccessibleButton>
</div>
@ -323,11 +332,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{recoveryKeyFeedback}
<DialogButtons
primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onRecoveryKeyNext}
onPrimaryButtonClick={this.onRecoveryKeyNext}
hasCancel={true}
cancelButton={_t("Go Back")}
cancelButtonClass='danger'
onCancel={this._onCancel}
onCancel={this.onCancel}
focus={false}
primaryDisabled={!this.state.recoveryKeyValid}
/>
@ -341,9 +350,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
title={title}
titleClass={titleClass}
>
<div>
{content}
</div>
<div>
{content}
</div>
</BaseDialog>
);
}

View file

@ -0,0 +1,66 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers";
const DEFAULT_NUM_FACES = 5;
interface IProps extends HTMLAttributes<HTMLSpanElement> {
room: Room;
onlyKnownUsers?: boolean;
numShown?: number;
}
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
let members = useRoomMembers(room);
// sort users with an explicit avatar first
const iteratees = [member => !!member.getMxcAvatarUrl()];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member));
}
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
return <div {...props} className="mx_FacePile">
<div className="mx_FacePile_faces">
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}>
<MemberAvatar member={member} width={28} height={28} />
</TextWithTooltip>;
}) }
</div>
{ onlyKnownUsers && <span>
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</div>
};
export default FacePile;

View file

@ -73,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = mediaFromMxc(idp.icon).getSquareThumbnailHttp(24);
const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24);
icon = <img src={src} height="24" width="24" alt={idp.name} />;
}

View file

@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component {
render() {
const Tooltip = sdk.getComponent("elements.Tooltip");
const {class: className, children, tooltip, tooltipClass, ...props} = this.props;
return (
<span onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={this.props.class}>
{this.props.children}
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
{children}
{this.state.hover && <Tooltip
label={this.props.tooltip}
tooltipClassName={this.props.tooltipClass}
label={tooltip}
tooltipClassName={tooltipClass}
className={"mx_TextWithTooltip_tooltip"} /> }
</span>
);

View file

@ -74,6 +74,11 @@ export default class MessageEvent extends React.Component {
'm.file': sdk.getComponent('messages.MFileBody'),
'm.audio': sdk.getComponent('messages.MAudioBody'),
'm.video': sdk.getComponent('messages.MVideoBody'),
// TODO: @@ TravisR: Use labs flag determination.
// MSC: https://github.com/matrix-org/matrix-doc/pull/2516
'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'),
...(this.props.overrideBodyTypes || {}),
};
const evTypes = {

View file

@ -216,12 +216,12 @@ export default class TextualBody extends React.Component {
}
_addLineNumbers(pre) {
// Calculate number of lines in pre
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
// Calculate number of lines in pre
const number = pre.innerHTML.split(/\n/).length;
// Iterate through lines starting with 1 (number of the first line is 1)
for (let i = 1; i < number; i++) {
for (let i = 1; i <= number; i++) {
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
}
}

View file

@ -52,7 +52,7 @@ const EncryptionInfo: React.FC<IProps> = ({
let text: string;
if (waitingForOtherParty) {
if (isSelfVerification) {
text = _t("Waiting for you to accept on your other session…");
text = _t("Accept on your other login…");
} else {
text = _t("Waiting for %(displayName)s to accept…", {
displayName: member.displayName || member.name || member.userId,

View file

@ -46,6 +46,7 @@ import {IDiff} from "../../../editor/diff";
import AutocompleteWrapperModel from "../../../editor/autocomplete";
import DocumentPosition from "../../../editor/position";
import {ICompletion} from "../../../autocomplete/Autocompleter";
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent";
// matches emoticons which follow the start of a line or whitespace
@ -93,6 +94,7 @@ interface IProps {
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
disabled?: boolean;
onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
@ -421,98 +423,94 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onKeyDown = (event: React.KeyboardEvent) => {
const model = this.props.model;
const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
let handled = false;
// format bold
if (modKey && event.key === Key.B) {
this.onFormatAction(Formatting.Bold);
handled = true;
// format italics
} else if (modKey && event.key === Key.I) {
this.onFormatAction(Formatting.Italics);
handled = true;
// format quote
} else if (modKey && event.key === Key.GREATER_THAN) {
this.onFormatAction(Formatting.Quote);
handled = true;
// redo
} else if ((!IS_MAC && modKey && event.key === Key.Y) ||
(IS_MAC && modKey && event.shiftKey && event.key === Key.Z)) {
if (this.historyManager.canRedo()) {
const {parts, caret} = this.historyManager.redo();
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyRedo");
}
handled = true;
// undo
} else if (modKey && event.key === Key.Z) {
if (this.historyManager.canUndo()) {
const {parts, caret} = this.historyManager.undo(this.props.model);
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyUndo");
}
handled = true;
// insert newline on Shift+Enter
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
this.insertText("\n");
handled = true;
// move selection to start of composer
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
setSelection(this.editorRef.current, model, {
index: 0,
offset: 0,
});
handled = true;
// move selection to end of composer
} else if (modKey && event.key === Key.END && !event.shiftKey) {
setSelection(this.editorRef.current, model, {
index: model.parts.length - 1,
offset: model.parts[model.parts.length - 1].text.length,
});
handled = true;
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
} else {
const metaOrAltPressed = event.metaKey || event.altKey;
const modifierPressed = metaOrAltPressed || event.shiftKey;
if (model.autoComplete && model.autoComplete.hasCompletions()) {
const autoComplete = model.autoComplete;
switch (event.key) {
case Key.ARROW_UP:
if (!modifierPressed) {
autoComplete.onUpArrow(event);
handled = true;
}
break;
case Key.ARROW_DOWN:
if (!modifierPressed) {
autoComplete.onDownArrow(event);
handled = true;
}
break;
case Key.TAB:
if (!metaOrAltPressed) {
autoComplete.onTab(event);
handled = true;
}
break;
case Key.ESCAPE:
if (!modifierPressed) {
autoComplete.onEscape(event);
handled = true;
}
break;
default:
return; // don't preventDefault on anything else
}
} else if (event.key === Key.TAB) {
this.tabCompleteName(event);
const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) {
case MessageComposerAction.FormatBold:
this.onFormatAction(Formatting.Bold);
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this.formatBarRef.current.hide();
}
break;
case MessageComposerAction.FormatItalics:
this.onFormatAction(Formatting.Italics);
handled = true;
break;
case MessageComposerAction.FormatQuote:
this.onFormatAction(Formatting.Quote);
handled = true;
break;
case MessageComposerAction.EditRedo:
if (this.historyManager.canRedo()) {
const {parts, caret} = this.historyManager.redo();
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyRedo");
}
handled = true;
break;
case MessageComposerAction.EditUndo:
if (this.historyManager.canUndo()) {
const {parts, caret} = this.historyManager.undo(this.props.model);
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyUndo");
}
handled = true;
break;
case MessageComposerAction.NewLine:
this.insertText("\n");
handled = true;
break;
case MessageComposerAction.MoveCursorToStart:
setSelection(this.editorRef.current, model, {
index: 0,
offset: 0,
});
handled = true;
break;
case MessageComposerAction.MoveCursorToEnd:
setSelection(this.editorRef.current, model, {
index: model.parts.length - 1,
offset: model.parts[model.parts.length - 1].text.length,
});
handled = true;
break;
}
if (handled) {
event.preventDefault();
event.stopPropagation();
return;
}
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
if (model.autoComplete && model.autoComplete.hasCompletions()) {
const autoComplete = model.autoComplete;
switch (autocompleteAction) {
case AutocompleteAction.PrevSelection:
autoComplete.onUpArrow(event);
handled = true;
break;
case AutocompleteAction.NextSelection:
autoComplete.onDownArrow(event);
handled = true;
break;
case AutocompleteAction.ApplySelection:
autoComplete.onTab(event);
handled = true;
break;
case AutocompleteAction.Cancel:
autoComplete.onEscape(event);
handled = true;
break;
default:
return; // don't preventDefault on anything else
}
} else if (autocompleteAction === AutocompleteAction.ApplySelection) {
this.tabCompleteName(event);
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this.formatBarRef.current.hide();
}
if (handled) {
event.preventDefault();
event.stopPropagation();
@ -672,6 +670,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
});
const shortcuts = {
@ -704,6 +705,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
aria-expanded={Boolean(this.state.autoComplete)}
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
dir="auto"
aria-disabled={this.props.disabled}
/>
</div>);
}

View file

@ -29,11 +29,10 @@ import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import classNames from 'classnames';
import {EventStatus} from 'matrix-js-sdk/src/models/event';
import BasicMessageComposer from "./BasicMessageComposer";
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent";
function _isReply(mxEvent) {
@ -136,38 +135,41 @@ export default class EditMessageComposer extends React.Component {
if (this._editorRef.isComposing(event)) {
return;
}
if (event.metaKey || event.altKey || event.shiftKey) {
return;
}
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
: event.key === Key.ENTER;
if (send) {
this._sendEdit();
event.preventDefault();
} else if (event.key === Key.ESCAPE) {
this._cancelEdit();
} else if (event.key === Key.ARROW_UP) {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) {
case MessageComposerAction.Send:
this._sendEdit();
event.preventDefault();
break;
case MessageComposerAction.CancelEditing:
this._cancelEdit();
break;
case MessageComposerAction.EditPrevMessage: {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false,
this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
}
break;
}
} else if (event.key === Key.ARROW_DOWN) {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
return;
case MessageComposerAction.EditNextMessage: {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
return;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
break;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
}
}

View file

@ -33,6 +33,7 @@ import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -187,6 +188,7 @@ export default class MessageComposer extends React.Component {
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
isComposerEmpty: true,
haveRecording: false,
};
}
@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component {
});
}
onVoiceUpdate = (haveRecording: boolean) => {
this.setState({haveRecording});
};
render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@ -346,17 +352,32 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
// TODO: @@ TravisR - Disabling the composer doesn't work
disabled={this.state.haveRecording}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
if (!this.state.haveRecording) {
controls.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
);
}
if (SettingsStore.getValue(UIFeature.Widgets) &&
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
!this.state.haveRecording) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (!this.state.isComposerEmpty) {
if (SettingsStore.getValue("feature_voice_messages")) {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
room={this.props.room}
onRecording={this.onVoiceUpdate} />);
}
if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(
<SendButton key="controls_send" onClick={this.sendMessage} />,
);

View file

@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent {
return (
<AccessibleTooltipButton
as="span"
role="button"
element="button"
type="button"
onClick={this.props.onClick}
title={this.props.label}
tooltip={tooltip}

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -28,6 +28,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
import {Action} from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher";
import SpaceStore from "../../../stores/SpaceStore";
import {showSpaceInvite} from "../../../utils/space";
const NewRoomIntro = () => {
const cli = useContext(MatrixClientContext);
@ -100,17 +102,48 @@ const NewRoomIntro = () => {
});
}
let buttons;
if (room.canInvite(cli.getUserId())) {
const onInviteClick = () => {
dis.dispatch({ action: "view_invite", roomId });
};
let parentSpace;
if (
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
) {
parentSpace = SpaceStore.instance.activeSpace;
}
let buttons;
if (parentSpace) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
showSpaceInvite(parentSpace);
}}
>
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")}
</AccessibleButton>
</div>
</div>;
}
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;

View file

@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react";
import { Dispatcher } from "flux";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@ -48,9 +49,8 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler";
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
import { EventType } from "matrix-js-sdk/src/@types/event";
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
@ -62,6 +62,7 @@ interface IProps {
onResize: () => void;
resizeNotifier: ResizeNotifier;
isMinimized: boolean;
activeSpace: Room;
}
interface IState {
@ -194,8 +195,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
: _t("You do not have permissions to add rooms to this space")}
/>
<IconizedContextMenuOption
label={_t("Explore space rooms")}
iconClassName="mx_RoomList_iconExplore"
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconBrowse"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -424,6 +425,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
};
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.props.activeSpace, initialText);
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
return this.state.suggestedRooms.map(room => {
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
@ -569,7 +575,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link"
onClick={this.onExplore}
>
{_t("Explore all public rooms")}
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (this.props.activeSpace) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
>
{_t("Invite people")}
</AccessibleButton> }
<AccessibleButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
>
{_t("Explore rooms")}
</AccessibleButton>
</div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {

View file

@ -51,6 +51,7 @@ import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import ExtraTile from "./ExtraTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
@ -470,18 +471,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
};
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
switch (ev.key) {
case Key.ARROW_LEFT:
const action = getKeyBindingsManager().getRoomListAction(ev);
switch (action) {
case RoomListAction.CollapseSection:
ev.stopPropagation();
if (this.state.isExpanded) {
// On ARROW_LEFT collapse the room sublist if it isn't already
// Collapse the room sublist if it isn't already
this.toggleCollapsed();
}
break;
case Key.ARROW_RIGHT: {
case RoomListAction.ExpandSection: {
ev.stopPropagation();
if (!this.state.isExpanded) {
// On ARROW_RIGHT expand the room sublist if it isn't already
// Expand the room sublist if it isn't already
this.toggleCollapsed();
} else if (this.sublistRef.current) {
// otherwise focus the first room

View file

@ -33,22 +33,22 @@ import ReplyThread from "../elements/ReplyThread";
import {parseEvent} from '../../../editor/deserialize';
import {findEditableEvent} from '../../../utils/EventUtils';
import SendHistoryManager from "../../../SendHistoryManager";
import {getCommand} from '../../../SlashCommands';
import {CommandCategories, getCommand} from '../../../SlashCommands';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
import {containsEmoji} from "../../../effects/utils";
import {CHAT_EFFECTS} from '../../../effects';
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import EMOJI_REGEX from 'emojibase-regex';
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import SettingsStore from '../../../settings/SettingsStore';
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -120,6 +120,7 @@ export default class SendMessageComposer extends React.Component {
permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
onChange: PropTypes.func,
disabled: PropTypes.bool,
};
static contextType = MatrixClientContext;
@ -147,60 +148,50 @@ export default class SendMessageComposer extends React.Component {
if (this._editorRef.isComposing(event)) {
return;
}
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const send = ctrlEnterToSend
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
: event.key === Key.ENTER && !hasModifier;
if (send) {
this._sendMessage();
event.preventDefault();
} else if (event.key === Key.ARROW_UP) {
this.onVerticalArrow(event, true);
} else if (event.key === Key.ARROW_DOWN) {
this.onVerticalArrow(event, false);
} else if (event.key === Key.ESCAPE) {
dis.dispatch({
action: 'reply_to_event',
event: null,
});
} else if (this._prepareToEncrypt) {
// This needs to be last!
this._prepareToEncrypt();
const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) {
case MessageComposerAction.Send:
this._sendMessage();
event.preventDefault();
break;
case MessageComposerAction.SelectPrevSendHistory:
case MessageComposerAction.SelectNextSendHistory: {
// Try select composer history
const selected = this.selectSendHistory(action === MessageComposerAction.SelectPrevSendHistory);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
event.preventDefault();
}
break;
}
case MessageComposerAction.EditPrevMessage:
// selection must be collapsed and caret at start
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
event.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
break;
case MessageComposerAction.CancelEditing:
dis.dispatch({
action: 'reply_to_event',
event: null,
});
break;
default:
if (this._prepareToEncrypt) {
// This needs to be last!
this._prepareToEncrypt();
}
}
};
onVerticalArrow(e, up) {
// arrows from an initial-caret composer navigates recent messages to edit
// ctrl-alt-arrows navigate send history
if (e.shiftKey || e.metaKey) return;
const shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
if (shouldSelectHistory) {
// Try select composer history
const selected = this.selectSendHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
}
} else if (shouldEditLastMessage) {
// selection must be collapsed and caret at start
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
}
}
// we keep sent messages/commands in a separate history (separate from undo history)
// so you can alt+up/down in them
selectSendHistory(up) {
@ -265,7 +256,7 @@ export default class SendMessageComposer extends React.Component {
const myReactionKeys = [...myReactionEvents]
.filter(event => !event.isRedacted())
.map(event => event.getRelation().key);
shouldReact = !myReactionKeys.includes(reaction);
shouldReact = !myReactionKeys.includes(reaction);
}
if (shouldReact) {
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
@ -290,15 +281,22 @@ export default class SendMessageComposer extends React.Component {
}
return text + part.text;
}, "");
return [getCommand(this.props.room.roomId, commandText), commandText];
const {cmd, args} = getCommand(commandText);
return [cmd, args, commandText];
}
async _runSlashCommand(fn) {
const cmd = fn();
let error = cmd.error;
if (cmd.promise) {
async _runSlashCommand(cmd, args) {
const result = cmd.run(this.props.room.roomId, args);
let messageContent;
let error = result.error;
if (result.promise) {
try {
await cmd.promise;
if (cmd.category === CommandCategories.messages) {
// The command returns a modified message that we need to pass on
messageContent = await result.promise;
} else {
await result.promise;
}
} catch (err) {
error = err;
}
@ -307,7 +305,7 @@ export default class SendMessageComposer extends React.Component {
console.error("Command failure: %s", error);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
// assume the error is a server error when the command is async
const isServerError = !!cmd.promise;
const isServerError = !!result.promise;
const title = isServerError ? _td("Server error") : _td("Command error");
let errText;
@ -325,6 +323,7 @@ export default class SendMessageComposer extends React.Component {
});
} else {
console.log("Command success.");
if (messageContent) return messageContent;
}
}
@ -333,13 +332,22 @@ export default class SendMessageComposer extends React.Component {
return;
}
const replyToEvent = this.props.replyToEvent;
let shouldSend = true;
let content;
if (!containsEmote(this.model) && this._isSlashCommand()) {
const [cmd, commandText] = this._getSlashCommand();
const [cmd, args, commandText] = this._getSlashCommand();
if (cmd) {
shouldSend = false;
this._runSlashCommand(cmd);
if (cmd.category === CommandCategories.messages) {
content = await this._runSlashCommand(cmd, args);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
}
} else {
this._runSlashCommand(cmd, args);
shouldSend = false;
}
} else {
// ask the user if their unknown command should be sent as a message
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
@ -374,11 +382,12 @@ export default class SendMessageComposer extends React.Component {
this._sendQuickReaction();
}
const replyToEvent = this.props.replyToEvent;
if (shouldSend) {
const startTime = CountlyAnalytics.getTimestamp();
const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
if (!content) {
content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
}
// don't bother sending an empty message
if (!content.body.trim()) return;
@ -453,12 +462,17 @@ export default class SendMessageComposer extends React.Component {
}
}
// should save state when editor has contents or reply is open
_shouldSaveStoredEditorState = () => {
return !this.model.isEmpty || this.props.replyToEvent;
}
_saveStoredEditorState = () => {
if (this.model.isEmpty) {
this._clearStoredEditorState();
} else {
if (this._shouldSaveStoredEditorState()) {
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
} else {
this._clearStoredEditorState();
}
}
@ -502,7 +516,7 @@ export default class SendMessageComposer extends React.Component {
_insertQuotedMessage(event) {
const {model} = this;
const {partCreator} = model;
const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true });
const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
// add two newlines
quoteParts.push(partCreator.newline());
quoteParts.push(partCreator.newline());
@ -556,6 +570,7 @@ export default class SendMessageComposer extends React.Component {
label={this.props.placeholder}
placeholder={this.props.placeholder}
onPaste={this._onPaste}
disabled={this.props.disabled}
/>
</div>
);

View file

@ -0,0 +1,88 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {_t} from "../../../languageHandler";
import React from "react";
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import classNames from "classnames";
interface IProps {
room: Room;
onRecording: (haveRecording: boolean) => void;
}
interface IState {
recorder?: VoiceRecorder;
}
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
public constructor(props) {
super(props);
this.state = {
recorder: null, // not recording by default
};
}
private onStartStopVoiceMessage = async () => {
// TODO: @@ TravisR: We do not want to auto-send on stop.
if (this.state.recorder) {
await this.state.recorder.stop();
const mxc = await this.state.recorder.upload();
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
body: "Voice message",
msgtype: "org.matrix.msc2516.voice",
url: mxc,
});
this.setState({recorder: null});
this.props.onRecording(false);
return;
}
const recorder = new VoiceRecorder(MatrixClientPeg.get());
await recorder.start();
this.props.onRecording(true);
// TODO: @@ TravisR: Run through EQ component
// recorder.frequencyData.onUpdate((freq) => {
// console.log('@@ UPDATE', freq);
// });
this.setState({recorder});
};
public render() {
const classes = classNames({
'mx_MessageComposer_button': !this.state.recorder,
'mx_MessageComposer_voiceMessage': !this.state.recorder,
'mx_VoiceRecordComposerTile_stop': !!this.state.recorder,
});
let tooltip = _t("Record a voice message");
if (!!this.state.recorder) {
// TODO: @@ TravisR: Change to match behaviour
tooltip = _t("Stop & send recording");
}
return (
<AccessibleTooltipButton
className={classes}
onClick={this.onStartStopVoiceMessage}
title={tooltip}
/>
);
}
}

View file

@ -190,7 +190,7 @@ export default class EventIndexPanel extends React.Component {
}
</div>
);
} else {
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
{
@ -208,6 +208,23 @@ export default class EventIndexPanel extends React.Component {
}
</div>
);
} else {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
<p>
{_t("Message search initilisation failed")}
</p>
{EventIndexPeg.error && (
<details>
<summary>{_t("Advanced")}</summary>
<code>
{EventIndexPeg.error.message}
</code>
</details>
)}
</div>
);
}
return eventIndexingSettings;

View file

@ -203,6 +203,7 @@ export class EmailAddress extends React.Component {
className="mx_ExistingEmailAddress_confirmBtn"
kind="primary_sm"
onClick={this.onContinueClick}
disabled={this.state.continueDisabled}
>
{_t("Complete")}
</AccessibleButton>

View file

@ -206,10 +206,10 @@ export default class GeneralUserSettingsTab extends React.Component {
_onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || "";
let errMsg = err.error || err.message || "";
if (err.httpStatus === 403) {
errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) {
} else if (!errMsg) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

View file

@ -21,7 +21,6 @@ import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types
import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import FormButton from "../elements/FormButton";
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings from "./SpaceBasicSettings";
@ -89,6 +88,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
},
},
spinner: false,
@ -108,7 +108,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
body = <React.Fragment>
<h2>{ _t("Create a space") }</h2>
<p>{ _t("Spaces are new ways to group rooms and people. " +
"To join an existing space youll need an invite") }</p>
"To join an existing space you'll need an invite.") }</p>
<SpaceCreateMenuType
title={_t("Public")}
@ -140,19 +140,17 @@ const SpaceCreateMenu = ({ onFinished }) => {
</h2>
<p>
{
_t("Give it a photo, name and description to help you identify it.")
_t("Add some details to help people recognise it.")
} {
_t("You can change these at any point.")
_t("You can change these anytime.")
}
</p>
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
<FormButton
label={busy ? _t("Creating...") : _t("Create")}
onClick={onSpaceCreateClick}
disabled={!name && !busy}
/>
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}>
{ busy ? _t("Creating...") : _t("Create") }
</AccessibleButton>
</React.Fragment>;
}

View file

@ -220,13 +220,19 @@ const SpacePanel = () => {
<SpaceButton
className={newClasses}
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
onClick={menuDisplayed ? closeMenu : openMenu}
onClick={menuDisplayed ? closeMenu : () => {
openMenu();
if (!isPanelCollapsed) setPanelCollapsed(true);
}}
isNarrow={isPanelCollapsed}
/>
</AutoHideScrollbar>
<AccessibleTooltipButton
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
onClick={() => {
setPanelCollapsed(!isPanelCollapsed);
if (menuDisplayed) closeMenu();
}}
title={expandCollapseButtonTitle}
/>
{ contextMenu }

View file

@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite";
interface IProps {
space: Room;
onFinished(): void;
onFinished?(): void;
}
const SpacePublicShare = ({ space, onFinished }: IProps) => {
@ -41,23 +41,24 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
const success = await copyPlaintext(permalinkCreator.forRoom());
const text = success ? _t("Copied!") : _t("Failed to copy");
setCopiedText(text);
await sleep(10);
await sleep(5000);
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
setCopiedText(_t("Click to copy"));
}
}}
>
{ _t("Share invite link") }
<h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span>
</AccessibleButton>
<AccessibleButton
className="mx_SpacePublicShare_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId);
onFinished();
if (onFinished) onFinished();
}}
>
{ _t("Invite by email or username") }
<h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span>
</AccessibleButton>
</div>;
};

View file

@ -30,20 +30,21 @@ import IconizedContextMenu, {
import {_t} from "../../../languageHandler";
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import {toRightOf} from "../../structures/ContextMenu";
import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showSpaceInvite,
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {ButtonEvent} from "../elements/AccessibleButton";
import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import SpacePublicShare from "./SpacePublicShare";
import {Action} from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {showRoomInviteDialog} from "../../../RoomInvite";
import InfoDialog from "../dialogs/InfoDialog";
import {EventType} from "matrix-js-sdk/src/@types/event";
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
interface IItemProps {
space?: Room;
@ -110,36 +111,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null});
};
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu
};
private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
if (this.props.space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite members"),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(this.props.space.roomId);
}
showSpaceInvite(this.props.space);
this.setState({contextMenuPosition: null}); // also close the menu
};
@ -170,6 +146,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null}); // also close the menu
};
private onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(this.context, this.props.space);
this.setState({contextMenuPosition: null}); // also close the menu
};
private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -193,9 +177,10 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, {
space: this.props.space,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu
};
@ -236,15 +221,22 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</IconizedContextMenuOptionList>;
}
let newRoomOption;
const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let newRoomSection;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomOption = (
newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("New room")}
label={_t("Create new room")}
onClick={this.onNewRoomClick}
/>
);
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
</IconizedContextMenuOptionList>;
}
contextMenu = <IconizedContextMenu
@ -258,11 +250,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</div>
<IconizedContextMenuOptionList first>
{ inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label={_t("Space Home")}
onClick={this.onHomeClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")}
@ -271,11 +258,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label={_t("Explore rooms")}
label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={this.onExploreRoomsClick}
/>
{ newRoomOption }
</IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection }
</IconizedContextMenu>;
}
@ -335,7 +322,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = childSpaces && childSpaces.length ?
<button
<AccessibleButton
className="mx_SpaceButton_toggleCollapse"
onClick={evt => this.toggleCollapse(evt)}
/> : null;

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import {XOR} from "../../../@types/common";
export interface IProps {
description: ReactNode;
detail?: ReactNode;
acceptLabel: string;
onAccept();
@ -33,14 +34,20 @@ interface IPropsExtended extends IProps {
const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
description,
detail,
acceptLabel,
rejectLabel,
onAccept,
onReject,
}) => {
const detailContent = detail ? <div className="mx_Toast_detail">
{detail}
</div> : null;
return <div>
<div className="mx_Toast_description">
{ description }
{description}
{detailContent}
</div>
<div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -140,11 +140,12 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
render() {
const {request} = this.props;
let nameLabel;
let description;
let detail;
if (request.isSelfVerification) {
if (this.state.device) {
nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", {
deviceName: this.state.device.getDisplayName(),
description = this.state.device.getDisplayName();
detail = _t("%(deviceId)s from %(ip)s", {
deviceId: this.state.device.deviceId,
ip: this.state.ip,
});
@ -152,13 +153,13 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
} else {
const userId = request.otherUserId;
const roomId = request.channel.roomId;
nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId;
description = roomId ? userLabelForEventRoom(userId, roomId) : userId;
// for legacy to_device verification requests
if (nameLabel === userId) {
if (description === userId) {
const client = MatrixClientPeg.get();
const user = client.getUser(userId);
if (user && user.displayName) {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
description = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
}
}
}
@ -167,7 +168,8 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
_t("Decline (%(counter)s)", {counter: this.state.counter});
return <GenericToast
description={nameLabel}
description={description}
detail={detail}
acceptLabel={_t("Accept")}
onAccept={this.accept}
rejectLabel={declineLabel}