Merge branch 'develop' into sort-imports

This commit is contained in:
Aaron Raimist 2021-10-27 21:50:56 -05:00
commit f3867ad0a9
107 changed files with 1722 additions and 1208 deletions

View file

@ -0,0 +1,92 @@
/*
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, { PureComponent, RefCallback, RefObject } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IInputProps } from "../elements/Field";
import { _t, _td } from "../../../languageHandler";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import * as Email from "../../../email";
interface IProps extends Omit<IInputProps, "onValidate"> {
id?: string;
fieldRef?: RefCallback<Field> | RefObject<Field>;
value: string;
autoFocus?: boolean;
label?: string;
labelRequired?: string;
labelInvalid?: string;
// When present, completely overrides the default validation rules.
validationRules?: (fieldState: IFieldState) => Promise<IValidationResult>;
onChange(ev: React.FormEvent<HTMLElement>): void;
onValidate?(result: IValidationResult): void;
}
@replaceableComponent("views.auth.EmailField")
class EmailField extends PureComponent<IProps> {
static defaultProps = {
label: _td("Email"),
labelRequired: _td("Enter email address"),
labelInvalid: _td("Doesn't look like a valid email address"),
};
public readonly validate = withValidation({
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t(this.props.labelRequired),
},
{
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t(this.props.labelInvalid),
},
],
});
onValidate = async (fieldState: IFieldState) => {
let validate = this.validate;
if (this.props.validationRules) {
validate = this.props.validationRules;
}
const result = await validate(fieldState);
if (this.props.onValidate) {
this.props.onValidate(result);
}
return result;
};
render() {
return <Field
id={this.props.id}
ref={this.props.fieldRef}
type="text"
label={_t(this.props.label)}
value={this.props.value}
autoFocus={this.props.autoFocus}
onChange={this.props.onChange}
onValidate={this.onValidate}
/>;
}
}
export default EmailField;

View file

@ -22,11 +22,11 @@ import SdkConfig from '../../../SdkConfig';
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton";
import CountlyAnalytics from "../../../CountlyAnalytics";
import withValidation from "../elements/Validation";
import * as Email from "../../../email";
import withValidation, { IValidationResult } from "../elements/Validation";
import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import EmailField from "./EmailField";
// For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -262,26 +262,8 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
return result;
};
private validateEmailRules = withValidation({
rules: [
{
key: "required",
test({ value, allowEmpty }) {
return allowEmpty || !!value;
},
invalid: () => _t("Enter email address"),
}, {
key: "email",
test: ({ value }) => !value || Email.looksValid(value),
invalid: () => _t("Doesn't look like a valid email address"),
},
],
});
private onEmailValidate = async (fieldState) => {
const result = await this.validateEmailRules(fieldState);
private onEmailValidate = (result: IValidationResult) => {
this.markFieldValid(LoginField.Email, result.valid);
return result;
};
private validatePhoneNumberRules = withValidation({
@ -332,12 +314,10 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
switch (loginType) {
case LoginField.Email:
classes.error = this.props.loginIncorrect && !this.props.username;
return <Field
return <EmailField
className={classNames(classes)}
name="username" // make it a little easier for browser's remember-password
key="email_input"
type="text"
label={_t("Email")}
placeholder="joe@example.com"
value={this.props.username}
onChange={this.onUsernameChanged}
@ -346,7 +326,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
disabled={this.props.disableSubmit}
autoFocus={autoFocus}
onValidate={this.onEmailValidate}
ref={field => this[LoginField.Email] = field}
fieldRef={field => this[LoginField.Email] = field}
/>;
case LoginField.MatrixId:
classes.error = this.props.loginIncorrect && !this.props.username;

View file

@ -24,8 +24,9 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
import withValidation from '../elements/Validation';
import withValidation, { IValidationResult } from '../elements/Validation';
import { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import EmailField from "./EmailField";
import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field';
@ -252,10 +253,8 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
});
};
private onEmailValidate = async fieldState => {
const result = await this.validateEmailRules(fieldState);
private onEmailValidate = (result: IValidationResult) => {
this.markFieldValid(RegistrationField.Email, result.valid);
return result;
};
private validateEmailRules = withValidation({
@ -425,14 +424,14 @@ export default class RegistrationForm extends React.PureComponent<IProps, IState
if (!this.showEmail()) {
return null;
}
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
const emailLabel = this.authStepIsRequired('m.login.email.identity') ?
_t("Email") :
_t("Email (optional)");
return <Field
ref={field => this[RegistrationField.Email] = field}
type="text"
label={emailPlaceholder}
return <EmailField
fieldRef={field => this[RegistrationField.Email] = field}
label={emailLabel}
value={this.state.email}
validationRules={this.validateEmailRules.bind(this)}
onChange={this.onEmailChange}
onValidate={this.onEmailValidate}
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email_focus")}

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { ComponentProps } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import classNames from "classnames";
import BaseAvatar from './BaseAvatar';
@ -83,8 +84,7 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
};
}
// TODO: type when js-sdk has types
private onRoomStateEvents = (ev: any) => {
private onRoomStateEvents = (ev: MatrixEvent) => {
if (!this.props.room ||
ev.getRoomId() !== this.props.room.roomId ||
ev.getType() !== 'm.room.avatar'

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { ComponentType } from 'react';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { logger } from "matrix-js-sdk/src/logger";
@ -85,7 +85,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{
matrixClient: MatrixClientPeg.get(),
},
@ -111,7 +113,9 @@ export default class LogoutDialog extends React.Component<IProps, IState> {
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
import(
"../../../async-components/views/dialogs/security/CreateKeyBackupDialog"
) as unknown as Promise<ComponentType<{}>>,
null, null, /* priority = */ false, /* static = */ true,
);
}

View file

@ -21,25 +21,14 @@ import { _t } from '../../../languageHandler';
import { IDialogProps } from "./IDialogProps";
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";
import EmailField from "../auth/EmailField";
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>();
@ -47,11 +36,11 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
const onSubmit = async (e) => {
e.preventDefault();
if (email) {
const valid = await fieldRef.current.validate({ allowEmpty: false });
const valid = await fieldRef.current.validate({});
if (!valid) {
fieldRef.current.focus();
fieldRef.current.validate({ allowEmpty: false, focused: true });
fieldRef.current.validate({ focused: true });
return;
}
}
@ -72,16 +61,15 @@ const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
b: sub => <b>{ sub }</b>,
}) }</p>
<form onSubmit={onSubmit}>
<Field
ref={fieldRef}
<EmailField
fieldRef={fieldRef}
autoFocus={true}
type="text"
label={_t("Email (optional)")}
value={email}
onChange={ev => {
setEmail(ev.target.value);
const target = ev.target as HTMLInputElement;
setEmail(target.value);
}}
onValidate={async fieldState => await validation(fieldState)}
onFocus={() => CountlyAnalytics.instance.track("onboarding_registration_email2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_registration_email2_blur")}
/>

View file

@ -44,6 +44,7 @@ import MessageEditHistoryDialog from "../dialogs/MessageEditHistoryDialog";
import EditMessageComposer from '../rooms/EditMessageComposer';
import LinkPreviewGroup from '../rooms/LinkPreviewGroup';
import { IBodyProps } from "./IBodyProps";
import RoomContext from "../../../contexts/RoomContext";
const MAX_HIGHLIGHT_LENGTH = 4096;
@ -62,6 +63,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private unmounted = false;
private pills: Element[] = [];
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
constructor(props) {
super(props);
@ -406,6 +410,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
userId: mxEvent.getSender(),
timelineRenderingType: this.context.timelineRenderingType,
});
};

View file

@ -75,6 +75,7 @@ import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialo
import { bulkSpaceBehaviour } from "../../../utils/space";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
export interface IDevice {
deviceId: string;
@ -377,6 +378,7 @@ const UserOptionsSection: React.FC<{
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
userId: member.userId,
timelineRenderingType: TimelineRenderingType.Room,
});
};

View file

@ -29,6 +29,7 @@ import {
formatRangeAsCode,
toggleInlineFormat,
replaceRangeAndMoveCaret,
formatRangeAsLink,
} from '../../../editor/operations';
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
@ -476,6 +477,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
switch (autocompleteAction) {
case AutocompleteAction.ForceComplete:
case AutocompleteAction.Complete:
this.historyManager.ensureLastChangesPushed(this.props.model);
this.modifiedFlag = true;
autoComplete.confirmCompletion();
handled = true;
break;
@ -705,6 +708,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
case Formatting.Quote:
formatRangeAsQuote(range);
break;
case Formatting.InsertLink:
formatRangeAsLink(range);
break;
}
};

View file

@ -46,6 +46,7 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import SettingsStore from "../../../settings/SettingsStore";
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
import RoomContext from '../../../contexts/RoomContext';
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body;
@ -498,7 +499,12 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
};
private onAction = (payload: ActionPayload) => {
if (payload.action === "edit_composer_insert" && this.editorRef.current) {
if (!this.editorRef.current) return;
if (payload.action === Action.ComposerInsert) {
if (payload.timelineRenderingType !== this.context.timelineRenderingType) return;
if (payload.composerType !== ComposerType.Edit) return;
if (payload.userId) {
this.editorRef.current?.insertMention(payload.userId);
} else if (payload.event) {
@ -506,7 +512,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
} else if (payload.text) {
this.editorRef.current?.insertPlaintext(payload.text);
}
} else if (payload.action === Action.FocusEditMessageComposer && this.editorRef.current) {
} else if (payload.action === Action.FocusEditMessageComposer) {
this.editorRef.current.focus();
}
};

View file

@ -22,7 +22,6 @@ import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { logger } from "matrix-js-sdk/src/logger";
import ReplyChain from "../elements/ReplyChain";
import { _t } from '../../../languageHandler';
@ -62,6 +61,9 @@ import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion";
import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads';
import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore';
import { logger } from "matrix-js-sdk/src/logger";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
[EventType.Sticker]: 'messages.MessageEvent',
@ -312,6 +314,8 @@ interface IProps {
// whether or not to display thread info
showThreadInfo?: boolean;
timelineRenderingType?: TimelineRenderingType;
}
interface IState {
@ -854,10 +858,11 @@ export default class EventTile extends React.Component<IProps, IState> {
}
onSenderProfileClick = () => {
const mxEvent = this.props.mxEvent;
if (!this.props.timelineRenderingType) return;
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
userId: mxEvent.getSender(),
userId: this.props.mxEvent.getSender(),
timelineRenderingType: this.props.timelineRenderingType,
});
};
@ -1090,7 +1095,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
if (needsSenderProfile && this.props.hideSender !== true) {
if (!this.props.tileShape) {
if (!this.props.tileShape || this.props.tileShape === TileShape.Thread) {
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}

View file

@ -253,7 +253,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private ref: React.RefObject<HTMLDivElement> = createRef();
private instanceId: number;
public static contextType = RoomContext;
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
static defaultProps = {
compact: false,
@ -399,13 +400,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
};
private addEmoji(emoji: string): boolean {
private addEmoji = (emoji: string): boolean => {
dis.dispatch<ComposerInsertPayload>({
action: Action.ComposerInsert,
text: emoji,
timelineRenderingType: this.context.timelineRenderingType,
});
return true;
}
};
private sendMessage = async () => {
if (this.state.haveRecording && this.voiceRecordingButton.current) {

View file

@ -27,6 +27,7 @@ export enum Formatting {
Strikethrough = "strikethrough",
Code = "code",
Quote = "quote",
InsertLink = "insert_link",
}
interface IProps {
@ -57,6 +58,7 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" visible={this.state.visible} />
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
<FormatButton label={_t("Insert link")} onClick={() => this.props.onAction(Formatting.InsertLink)} icon="InsertLink" visible={this.state.visible} />
</div>);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactComponentElement } from "react";
import React, { createRef, ReactComponentElement } from "react";
import { Dispatcher } from "flux";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { RovingTabIndexProvider, IState as IRovingTabIndexState } from "../../../accessibility/RovingTabIndex";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
@ -54,7 +54,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
onFocus: (ev: React.FocusEvent) => void;
onBlur: (ev: React.FocusEvent) => void;
onResize: () => void;
@ -249,6 +249,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private dispatcherRef;
private customTagStoreRef;
private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>();
constructor(props: IProps) {
super(props);
@ -505,6 +506,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
});
}
public focus(): void {
// focus the first focusable element in this aria treeview widget
[...this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]')]
.find(e => e.offsetParent !== null)?.focus();
}
public render() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
@ -584,7 +591,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const sublists = this.renderSublists();
return (
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
{ ({ onKeyDownHandler }) => (
<div
onFocus={this.props.onFocus}
@ -593,6 +600,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
ref={this.treeRef}
>
{ sublists }
{ explorePrompt }

View file

@ -58,6 +58,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import RoomContext from '../../../contexts/RoomContext';
import DocumentPosition from "../../../editor/position";
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
function addReplyToMessageContent(
content: IContent,
@ -591,7 +592,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
this.editorRef.current?.focus();
}
break;
case "send_composer_insert":
case Action.ComposerInsert:
if (payload.timelineRenderingType !== this.context.timelineRenderingType) break;
if (payload.composerType !== ComposerType.Send) break;
if (payload.userId) {
this.editorRef.current?.insertMention(payload.userId);
} else if (payload.event) {

View file

@ -15,10 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixClient } from "matrix-js-sdk/src/client";
import Field from "../elements/Field";
import React, { ComponentType } from 'react';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Spinner from '../elements/Spinner';
@ -29,6 +27,7 @@ import PassphraseField from "../auth/PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../auth/RegistrationForm';
import { MatrixClient } from "matrix-js-sdk/src/client";
import SetEmailDialog from "../dialogs/SetEmailDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
@ -187,7 +186,9 @@ export default class ChangePassword extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{
matrixClient: MatrixClientPeg.get(),
},

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, { ComponentType } from 'react';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
@ -92,14 +92,18 @@ export default class CryptographyPanel extends React.Component<IProps, IState> {
private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
import(
'../../../async-components/views/dialogs/security/ExportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ matrixClient: MatrixClientPeg.get() },
);
};
private onImportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
import(
'../../../async-components/views/dialogs/security/ImportE2eKeysDialog'
) as unknown as Promise<ComponentType<{}>>,
{ matrixClient: MatrixClientPeg.get() },
);
};

View file

@ -15,10 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
import { logger } from "matrix-js-sdk/src/logger";
import React, { ComponentType } from 'react';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
@ -30,6 +27,8 @@ import QuestionDialog from '../dialogs/QuestionDialog';
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
import { accessSecretStorage } from '../../../SecurityManager';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import { TrustInfo } from "matrix-js-sdk/src/crypto/backup";
interface IState {
loading: boolean;
@ -44,6 +43,8 @@ interface IState {
sessionsRemaining: number;
}
import { logger } from "matrix-js-sdk/src/logger";
@replaceableComponent("views.settings.SecureBackupPanel")
export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private unmounted = false;
@ -169,7 +170,9 @@ export default class SecureBackupPanel extends React.PureComponent<{}, IState> {
private startNewBackup = (): void => {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
import(
'../../../async-components/views/dialogs/security/CreateKeyBackupDialog'
) as unknown as Promise<ComponentType<{}>>,
{
onFinished: () => {
this.loadBackupStatus();

View file

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import { enumerateThemes } from "../../../theme";
import { enumerateThemes, findHighContrastTheme, findNonHighContrastTheme, isHighContrastTheme } from "../../../theme";
import ThemeWatcher from "../../../settings/watchers/ThemeWatcher";
import AccessibleButton from "../elements/AccessibleButton";
import dis from "../../../dispatcher/dispatcher";
@ -159,7 +159,37 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
this.setState({ customThemeUrl: e.target.value });
};
public render() {
private renderHighContrastCheckbox(): React.ReactElement<HTMLDivElement> {
if (
!this.state.useSystemTheme && (
findHighContrastTheme(this.state.theme) ||
isHighContrastTheme(this.state.theme)
)
) {
return <div>
<StyledCheckbox
checked={isHighContrastTheme(this.state.theme)}
onChange={(e) => this.highContrastThemeChanged(e.target.checked)}
>
{ _t( "Use high contrast" ) }
</StyledCheckbox>
</div>;
}
}
private highContrastThemeChanged(checked: boolean): void {
let newTheme: string;
if (checked) {
newTheme = findHighContrastTheme(this.state.theme);
} else {
newTheme = findNonHighContrastTheme(this.state.theme);
}
if (newTheme) {
this.onThemeChange(newTheme);
}
}
public render(): React.ReactElement<HTMLDivElement> {
const themeWatcher = new ThemeWatcher();
let systemThemeSection: JSX.Element;
if (themeWatcher.isSystemThemeSupported()) {
@ -210,7 +240,8 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
// XXX: replace any type here
const themes = Object.entries<any>(enumerateThemes())
.map(p => ({ id: p[0], name: p[1] })); // convert pairs to objects for code readability
.map(p => ({ id: p[0], name: p[1] })) // convert pairs to objects for code readability
.filter(p => !isHighContrastTheme(p.id));
const builtInThemes = themes.filter(p => !p.id.startsWith("custom-"));
const customThemes = themes.filter(p => !builtInThemes.includes(p))
.sort((a, b) => compare(a.name, b.name));
@ -229,12 +260,21 @@ export default class ThemeChoicePanel extends React.Component<IProps, IState> {
className: "mx_ThemeSelector_" + t.id,
}))}
onChange={this.onThemeChange}
value={this.state.useSystemTheme ? undefined : this.state.theme}
value={this.apparentSelectedThemeId()}
outlined
/>
</div>
{ this.renderHighContrastCheckbox() }
{ customThemeForm }
</div>
);
}
apparentSelectedThemeId() {
if (this.state.useSystemTheme) {
return undefined;
}
const nonHighContrast = findNonHighContrastTheme(this.state.theme);
return nonHighContrast ? nonHighContrast : this.state.theme;
}
}

View file

@ -43,7 +43,6 @@ import SpaceStore, {
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { Key } from "../../../Keyboard";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
import IconizedContextMenu, {
@ -228,75 +227,12 @@ const SpacePanel = () => {
return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel");
}, []);
const onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.defaultPrevented) return;
let handled = true;
switch (ev.key) {
case Key.ARROW_UP:
onMoveFocus(ev.target as Element, true);
break;
case Key.ARROW_DOWN:
onMoveFocus(ev.target as Element, false);
break;
default:
handled = false;
}
if (handled) {
// consume all other keys in context menu
ev.stopPropagation();
ev.preventDefault();
}
};
const onMoveFocus = (element: Element, up: boolean) => {
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes: DOMTokenList;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
element = up ? element.lastElementChild : element.firstElementChild;
descending = true;
}
classes = element.classList;
}
} while (element && !classes.contains("mx_SpaceButton"));
if (element) {
(element as HTMLElement).focus();
}
};
return (
<DragDropContext onDragEnd={result => {
if (!result.destination) return; // dropped outside the list
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
}}>
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
<RovingTabIndexProvider handleHomeEnd handleUpDown>
{ ({ onKeyDownHandler }) => (
<ul
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}

View file

@ -279,6 +279,8 @@ export default class CallView extends React.Component<IProps, IState> {
if (window.electron?.getDesktopCapturerSources) {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
if (!source) return;
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
} else {
isScreensharing = await this.props.call.setScreensharingEnabled(true);
@ -545,6 +547,7 @@ export default class CallView extends React.Component<IProps, IState> {
<div
className={classes}
onMouseMove={this.onMouseMove}
ref={this.contentRef}
>
{ sidebar }
<div className="mx_CallView_voice_avatarsContainer">