Merge remote-tracking branch 'origin/develop' into jaywink/elementPro

This commit is contained in:
Jason Robinson 2020-12-16 14:45:16 +02:00
commit 5aa24b97cd
145 changed files with 7274 additions and 2269 deletions

View file

@ -23,15 +23,17 @@ import { _t } from '../../../languageHandler';
*/
export default class ConfirmRedactDialog extends React.Component {
render() {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
return (
<QuestionDialog onFinished={this.props.onFinished}
<TextInputDialog onFinished={this.props.onFinished}
title={_t("Confirm Removal")}
description={
_t("Are you sure you wish to remove (delete) this event? " +
"Note that if you delete a room name or topic change, it could undo the change.")}
placeholder={_t("Reason (optional)")}
focus
button={_t("Remove")}>
</QuestionDialog>
</TextInputDialog>
);
}
}

View file

@ -31,6 +31,7 @@ export default class InfoDialog extends React.Component {
onFinished: PropTypes.func,
hasCloseButton: PropTypes.bool,
onKeyDown: PropTypes.func,
fixedWidth: PropTypes.bool,
};
static defaultProps = {
@ -54,6 +55,7 @@ export default class InfoDialog extends React.Component {
contentId='mx_Dialog_content'
hasCancel={this.props.hasCloseButton}
onKeyDown={this.props.onKeyDown}
fixedWidth={this.props.fixedWidth}
>
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
{ this.props.description }

View file

@ -15,13 +15,12 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import {RoomMember} from "matrix-js-sdk/src/matrix";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import SdkConfig from "../../../SdkConfig";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import * as Email from "../../../email";
@ -132,12 +131,12 @@ class ThreepidMember extends Member {
}
}
class DMUserTile extends React.PureComponent {
static propTypes = {
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
onRemove: PropTypes.func, // takes 1 argument, the member being removed
};
interface IDMUserTileProps {
member: RoomMember;
onRemove: (RoomMember) => any;
}
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
_onRemove = (e) => {
// Stop the browser from highlighting text
e.preventDefault();
@ -173,7 +172,9 @@ class DMUserTile extends React.PureComponent {
className='mx_InviteDialog_userTile_remove'
onClick={this._onRemove}
>
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
<img src={require("../../../../res/img/icon-pill-remove.svg")}
alt={_t('Remove')} width={8} height={8}
/>
</AccessibleButton>
);
}
@ -190,15 +191,15 @@ class DMUserTile extends React.PureComponent {
}
}
class DMRoomTile extends React.PureComponent {
static propTypes = {
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
lastActiveTs: PropTypes.number,
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
highlightWord: PropTypes.string,
isSelected: PropTypes.bool,
};
interface IDMRoomTileProps {
member: RoomMember;
lastActiveTs: number;
onToggle: (RoomMember) => any;
highlightWord: string;
isSelected: boolean;
}
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
_onClick = (e) => {
// Stop the browser from highlighting text
e.preventDefault();
@ -298,28 +299,45 @@ class DMRoomTile extends React.PureComponent {
}
}
export default class InviteDialog extends React.PureComponent {
static propTypes = {
// Takes an array of user IDs/emails to invite.
onFinished: PropTypes.func.isRequired,
interface IInviteDialogProps {
// Takes an array of user IDs/emails to invite.
onFinished: (toInvite?: string[]) => any;
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: PropTypes.string,
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: string,
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: PropTypes.string,
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string,
// Initial value to populate the filter with
initialText: PropTypes.string,
};
// Initial value to populate the filter with
initialText: string,
}
interface IInviteDialogState {
targets: RoomMember[]; // array of Member objects (see interface above)
filterText: string;
recents: { user: Member, userId: string }[];
numRecentsShown: number;
suggestions: { user: Member, userId: string }[];
numSuggestionsShown: number;
serverResultsMixin: { user: Member, userId: string }[];
threepidResultsMixin: { user: Member, userId: string}[];
canUseIdentityServer: boolean;
tryingIdentityServer: boolean;
// These two flags are used for the 'Go' button to communicate what is going on.
busy: boolean,
errorText: string,
}
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
static defaultProps = {
kind: KIND_DM,
initialText: "",
};
_debounceTimer: number = null;
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
_editorRef: any = null;
constructor(props) {
@ -348,8 +366,8 @@ export default class InviteDialog extends React.PureComponent {
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this._buildSuggestions(alreadyInvited),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
serverResultsMixin: [],
threepidResultsMixin: [],
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
tryingIdentityServer: false,
@ -367,7 +385,7 @@ export default class InviteDialog extends React.PureComponent {
}
}
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
@ -430,7 +448,7 @@ export default class InviteDialog extends React.PureComponent {
return recents;
}
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember} {
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
const maxConsideredMembers = 200;
const joinedRooms = MatrixClientPeg.get().getRooms()
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
@ -470,7 +488,7 @@ export default class InviteDialog extends React.PureComponent {
}, {});
// Generates { userId: {member, numRooms, score} }
const memberScores = Object.values(memberRooms).reduce((scores, entry) => {
const memberScores = Object.values(memberRooms).reduce((scores, entry: {member: RoomMember, rooms: Room[]}) => {
const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0);
const maxRange = maxConsideredMembers * entry.rooms.length;
scores[entry.member.userId] = {
@ -603,7 +621,7 @@ export default class InviteDialog extends React.PureComponent {
return;
}
const createRoomOptions = {inlineErrors: true};
const createRoomOptions = {inlineErrors: true} as any; // XXX: Type out `createRoomOptions`
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
@ -620,7 +638,7 @@ export default class InviteDialog extends React.PureComponent {
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve();
let createRoomPromise = Promise.resolve(null) as Promise<string | null | boolean>;
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
@ -990,7 +1008,8 @@ export default class InviteDialog extends React.PureComponent {
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
if (this.state.filterText && hasMixins && kind === 'suggestions') {
// We don't want to duplicate members though, so just exclude anyone we've already seen.
const notAlreadyExists = (u: Member): boolean => {
// The type of u is a pain to define but members of both mixins have the 'userId' property
const notAlreadyExists = (u: any): boolean => {
return !sourceMembers.some(m => m.userId === u.userId)
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
@ -1169,7 +1188,8 @@ export default class InviteDialog extends React.PureComponent {
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
const inviteText = _t("This won't invite them to %(communityName)s. " +
const inviteText = _t(
"This won't invite them to %(communityName)s. " +
"To invite someone to %(communityName)s, click <a>here</a>",
{communityName}, {
userId: () => {
@ -1209,7 +1229,9 @@ export default class InviteDialog extends React.PureComponent {
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
{sub}
</a>,
},
);
} else {
@ -1220,7 +1242,9 @@ export default class InviteDialog extends React.PureComponent {
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
{sub}
</a>,
},
);
}

View file

@ -0,0 +1,96 @@
/*
Copyright 2020 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 * as React from "react";
import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
import {useRef, useState} from "react";
import Field from "../elements/Field";
import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation";
import * as Email from "../../../email";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
interface IProps extends IDialogProps {
onFinished(continued: boolean, email?: string): void;
}
const validation = withValidation({
rules: [
{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
},
],
});
const RegistrationEmailPromptDialog: React.FC<IProps> = ({onFinished}) => {
const [email, setEmail] = useState("");
const fieldRef = useRef<Field>();
const onSubmit = async () => {
if (email) {
const valid = await fieldRef.current.validate({ allowEmpty: false });
if (!valid) {
fieldRef.current.focus();
fieldRef.current.validate({ allowEmpty: false, focused: true });
return;
}
}
onFinished(true, email);
};
return <BaseDialog
title={_t("Continuing without email")}
className="mx_RegistrationEmailPromptDialog"
contentId="mx_RegistrationEmailPromptDialog"
onFinished={() => onFinished(false)}
fixedWidth={false}
>
<div className="mx_Dialog_content" id="mx_RegistrationEmailPromptDialog">
<p>{_t("Just a heads up, if you don't add an email and forget your password, you could " +
"<b>permanently lose access to your account</b>.", {}, {
b: sub => <b>{sub}</b>,
})}</p>
<form onSubmit={onSubmit}>
<Field
ref={fieldRef}
type="text"
label={_t("Email (optional)")}
value={email}
onChange={ev => {
setEmail(ev.target.value);
}}
onValidate={async fieldState => await validation(fieldState)}
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
/>
</form>
</div>
<DialogButtons
primaryButton={_t("Continue")}
onPrimaryButtonClick={onSubmit}
hasCancel={false}
/>
</BaseDialog>;
};
export default RegistrationEmailPromptDialog;

View file

@ -53,9 +53,9 @@ export default class RoomSettingsDialog extends React.Component {
}
_onAction = (payload) => {
// When room changes below us, close the room settings
// When view changes below us, close the room settings
// whilst the modal is open this can only be triggered when someone hits Leave Room
if (payload.action === 'view_next_room') {
if (payload.action === 'view_home_page') {
this.props.onFinished();
}
};

View file

@ -0,0 +1,235 @@
/*
Copyright 2020 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, {createRef} from "react";
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import SdkConfig from "../../../SdkConfig";
import Field from "../elements/Field";
import StyledRadioButton from "../elements/StyledRadioButton";
import TextWithTooltip from "../elements/TextWithTooltip";
import withValidation, {IFieldState} from "../elements/Validation";
interface IProps {
title?: string;
serverConfig: ValidatedServerConfig;
onFinished(config?: ValidatedServerConfig): void;
}
interface IState {
defaultChosen: boolean;
otherHomeserver: string;
}
export default class ServerPickerDialog extends React.PureComponent<IProps, IState> {
private readonly defaultServer: ValidatedServerConfig;
private readonly fieldRef = createRef<Field>();
private validatedConf: ValidatedServerConfig;
constructor(props) {
super(props);
const config = SdkConfig.get();
this.defaultServer = config["validated_server_config"] as ValidatedServerConfig;
const { serverConfig } = this.props;
let otherHomeserver = "";
if (!serverConfig.isDefault) {
if (serverConfig.isNameResolvable && serverConfig.hsName) {
otherHomeserver = serverConfig.hsName;
} else {
otherHomeserver = serverConfig.hsUrl;
}
}
this.state = {
defaultChosen: serverConfig.isDefault,
otherHomeserver,
};
}
private onDefaultChosen = () => {
this.setState({ defaultChosen: true });
};
private onOtherChosen = () => {
this.setState({ defaultChosen: false });
};
private onHomeserverChange = (ev) => {
this.setState({ otherHomeserver: ev.target.value });
};
// TODO: Do we want to support .well-known lookups here?
// If for some reason someone enters "matrix.org" for a URL, we could do a lookup to
// find their homeserver without demanding they use "https://matrix.org"
private validate = withValidation<this, { error?: string }>({
deriveData: async ({ value }) => {
let hsUrl = value.trim(); // trim to account for random whitespace
// if the URL has no protocol, try validate it as a serverName via well-known
if (!hsUrl.includes("://")) {
try {
const discoveryResult = await AutoDiscovery.findClientConfig(hsUrl);
this.validatedConf = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(hsUrl, discoveryResult);
return {}; // we have a validated config, we don't need to try the other paths
} catch (e) {
console.error(`Attempted ${hsUrl} as a server_name but it failed`, e);
}
}
// if we got to this stage then either the well-known failed or the URL had a protocol specified,
// so validate statically only. If the URL has no protocol, default to https.
if (!hsUrl.includes("://")) {
hsUrl = "https://" + hsUrl;
}
try {
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl);
return {};
} catch (e) {
console.error(e);
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
if (stateForError.isFatalError) {
let error = _t("Unable to validate homeserver");
if (e.translatedMessage) {
error = e.translatedMessage;
}
return { error };
}
// try to carry on anyway
try {
this.validatedConf = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, null, true);
return {};
} catch (e) {
console.error(e);
return { error: _t("Invalid URL") };
}
}
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Specify a homeserver"),
}, {
key: "valid",
test: async function({ value }, { error }) {
if (!value) return true;
return !error;
},
invalid: function({ error }) {
return error;
},
},
],
});
private onHomeserverValidate = (fieldState: IFieldState) => this.validate(fieldState);
private onSubmit = async (ev) => {
ev.preventDefault();
const valid = await this.fieldRef.current.validate({ allowEmpty: false });
if (!valid) {
this.fieldRef.current.focus();
this.fieldRef.current.validate({ allowEmpty: false, focused: true });
return;
}
this.props.onFinished(this.validatedConf);
};
public render() {
let text;
if (this.defaultServer.hsName === "matrix.org") {
text = _t("Matrix.org is the biggest public homeserver in the world, so its a good place for many.");
}
let defaultServerName = this.defaultServer.hsName;
if (this.defaultServer.hsNameIsDifferent) {
defaultServerName = (
<TextWithTooltip class="mx_Login_underlinedServerName" tooltip={this.defaultServer.hsUrl}>
{this.defaultServer.hsName}
</TextWithTooltip>
);
}
return <BaseDialog
title={this.props.title || _t("Sign into your homeserver")}
className="mx_ServerPickerDialog"
contentId="mx_ServerPickerDialog"
onFinished={this.props.onFinished}
fixedWidth={false}
hasCancel={true}
>
<form className="mx_Dialog_content" id="mx_ServerPickerDialog" onSubmit={this.onSubmit}>
<p>
{_t("We call the places where you can host your account homeservers.")} {text}
</p>
<StyledRadioButton
name="defaultChosen"
value="true"
checked={this.state.defaultChosen}
onChange={this.onDefaultChosen}
>
{defaultServerName}
</StyledRadioButton>
<StyledRadioButton
name="defaultChosen"
value="false"
className="mx_ServerPickerDialog_otherHomeserverRadio"
checked={!this.state.defaultChosen}
onChange={this.onOtherChosen}
>
<Field
type="text"
className="mx_ServerPickerDialog_otherHomeserver"
label={_t("Other homeserver")}
onChange={this.onHomeserverChange}
onClick={this.onOtherChosen}
ref={this.fieldRef}
onValidate={this.onHomeserverValidate}
value={this.state.otherHomeserver}
validateOnChange={false}
validateOnFocus={false}
/>
</StyledRadioButton>
<p>
{_t("Use your preferred Matrix homeserver if you have one, or host your own.")}
</p>
<AccessibleButton className="mx_ServerPickerDialog_continue" kind="primary" onClick={this.onSubmit}>
{_t("Continue")}
</AccessibleButton>
<h4>{_t("Learn more")}</h4>
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener">
{_t("About homeservers")}
</a>
</form>
</BaseDialog>;
}
}

View file

@ -146,7 +146,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const events = this.props.target.getLiveTimeline().getEvents();
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
matrixToUrl = this.state.permalinkCreator.forShareableRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
matrixToUrl = makeUserPermalink(this.props.target.userId);