Merge branch 'develop' into travis/voice-messages/waveform

This commit is contained in:
Travis Ralston 2021-03-29 22:59:51 -06:00
commit 54412878a1
77 changed files with 2325 additions and 978 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";
@ -110,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>;
@ -128,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>
@ -172,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 }
@ -185,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);
@ -200,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

@ -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
@ -422,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();

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

@ -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

@ -38,17 +38,17 @@ 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);
@ -148,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) {
@ -266,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", {
@ -472,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();
}
}
@ -521,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());

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

@ -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

@ -74,6 +74,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
this.state = {
autoLaunch: false,
autoLaunchSupported: false,
warnBeforeExit: true,
warnBeforeExitSupported: false,
alwaysShowMenuBar: true,
alwaysShowMenuBarSupported: false,
minimizeToTray: true,
@ -96,6 +98,12 @@ export default class PreferencesUserSettingsTab extends React.Component {
autoLaunch = await platform.getAutoLaunchEnabled();
}
const warnBeforeExitSupported = await platform.supportsWarnBeforeExit();
let warnBeforeExit = false;
if (warnBeforeExitSupported) {
warnBeforeExit = await platform.shouldWarnBeforeExit();
}
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
let alwaysShowMenuBar = true;
if (alwaysShowMenuBarSupported) {
@ -111,6 +119,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
this.setState({
autoLaunch,
autoLaunchSupported,
warnBeforeExit,
warnBeforeExitSupported,
alwaysShowMenuBarSupported,
alwaysShowMenuBar,
minimizeToTraySupported,
@ -122,6 +132,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
};
_onWarnBeforeExitChange = (checked) => {
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked}));
}
_onAlwaysShowMenuBarChange = (checked) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked}));
};
@ -161,6 +175,14 @@ export default class PreferencesUserSettingsTab extends React.Component {
label={_t('Start automatically after system login')} />;
}
let warnBeforeExitOption = null;
if (this.state.warnBeforeExitSupported) {
warnBeforeExitOption = <LabelledToggleSwitch
value={this.state.warnBeforeExit}
onChange={this._onWarnBeforeExitChange}
label={_t('Warn before quitting')} />;
}
let autoHideMenuOption = null;
if (this.state.alwaysShowMenuBarSupported) {
autoHideMenuOption = <LabelledToggleSwitch
@ -202,6 +224,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
{minimizeToTrayOption}
{autoHideMenuOption}
{autoLaunchOption}
{warnBeforeExitOption}
<Field
label={_t('Autocomplete delay (ms)')}
type='number'

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,
@ -148,11 +148,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
<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

@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite";
interface IProps {
space: Room;
onFinished(): void;
onFinished?(): void;
}
const SpacePublicShare = ({ space, onFinished }: IProps) => {
@ -54,7 +54,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
className="mx_SpacePublicShare_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId);
onFinished();
if (onFinished) onFinished();
}}
>
<h3>{ _t("Invite people") }</h3>

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}