Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/ts/8

 Conflicts:
	src/components/structures/AutoHideScrollbar.tsx
This commit is contained in:
Michael Telatynski 2021-06-24 16:42:03 +01:00
commit c5a1da49ce
97 changed files with 2840 additions and 1523 deletions

View file

@ -62,6 +62,8 @@ export default function AccessibleButton({
disabled,
inputRef,
className,
onKeyDown,
onKeyUp,
...restProps
}: IProps) {
const newProps: IAccessibleButtonProps = restProps;
@ -83,6 +85,8 @@ export default function AccessibleButton({
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
} else {
onKeyDown?.(e);
}
};
newProps.onKeyUp = (e) => {
@ -94,6 +98,8 @@ export default function AccessibleButton({
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
} else {
onKeyUp?.(e);
}
};
}

View file

@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import { UserAddressType } from '../../../UserAddress.js';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
import { UserAddressType } from '../../../UserAddress';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.elements.AddressTile")
export default class AddressTile extends React.Component {

View file

@ -18,7 +18,6 @@ limitations under the License.
import TagTile from './TagTile';
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index';
@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
</ContextMenu>
);
}
return <div>
<Draggable
key={props.tag}
draggableId={props.tag}
index={props.index}
type="draggable-TagTile"
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile
{...props}
contextMenuButtonRef={handle}
menuDisplayed={menuDisplayed}
openMenu={openMenu}
/>
</div>
)}
</Draggable>
return <>
<TagTile
{...props}
contextMenuButtonRef={handle}
menuDisplayed={menuDisplayed}
openMenu={openMenu}
/>
{contextMenu}
</div>;
</>;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2017, 2019 New Vector Ltd.
Copyright 2017-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.
@ -14,48 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from '../../../languageHandler';
import React from "react";
import { _t } from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
export class EditableItem extends React.Component {
static propTypes = {
index: PropTypes.number,
value: PropTypes.string,
onRemove: PropTypes.func,
interface IItemProps {
index?: number;
value?: string;
onRemove?(index: number): void;
}
interface IItemState {
verifyRemove: boolean;
}
export class EditableItem extends React.Component<IItemProps, IItemState> {
public state = {
verifyRemove: false,
};
constructor() {
super();
this.state = {
verifyRemove: false,
};
}
_onRemove = (e) => {
private onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: true});
this.setState({ verifyRemove: true });
};
_onDontRemove = (e) => {
private onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
_onActuallyRemove = (e) => {
private onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onRemove) this.props.onRemove(this.props.index);
this.setState({verifyRemove: false});
this.setState({ verifyRemove: false });
};
render() {
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
{_t("Are you sure?")}
</span>
<AccessibleButton
onClick={this._onActuallyRemove}
onClick={this.onActuallyRemove}
kind="primary_sm"
className="mx_EditableItem_confirmBtn"
>
{_t("Yes")}
</AccessibleButton>
<AccessibleButton
onClick={this._onDontRemove}
onClick={this.onDontRemove}
kind="danger_sm"
className="mx_EditableItem_confirmBtn"
>
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
return (
<div className="mx_EditableItem">
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
<span className="mx_EditableItem_item">{this.props.value}</span>
</div>
);
}
}
interface IProps {
id: string;
items: string[];
itemsLabel?: string;
noItemsLabel?: string;
placeholder?: string;
newItem?: string;
canEdit?: boolean;
canRemove?: boolean;
suggestionsListId?: string;
onItemAdded?(item: string): void;
onItemRemoved?(index: number): void;
onNewItemChanged?(item: string): void;
}
@replaceableComponent("views.elements.EditableItemList")
export default class EditableItemList extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
itemsLabel: PropTypes.string,
noItemsLabel: PropTypes.string,
placeholder: PropTypes.string,
newItem: PropTypes.string,
onItemAdded: PropTypes.func,
onItemRemoved: PropTypes.func,
onNewItemChanged: PropTypes.func,
canEdit: PropTypes.bool,
canRemove: PropTypes.bool,
};
_onItemAdded = (e) => {
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
protected onItemAdded = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
};
_onItemRemoved = (index) => {
protected onItemRemoved = (index) => {
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
};
_onNewItemChanged = (e) => {
protected onNewItemChanged = (e) => {
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
};
_renderNewItemField() {
protected renderNewItemField() {
return (
<form
onSubmit={this._onItemAdded}
onSubmit={this.onItemAdded}
autoComplete="off"
noValidate={true}
className="mx_EditableItemList_newItem"
>
<Field label={this.props.placeholder} type="text"
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
list={this.props.suggestionsListId} />
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
{_t("Add")}
<Field
label={this.props.placeholder}
type="text"
autoComplete="off"
value={this.props.newItem || ""}
onChange={this.onNewItemChanged}
list={this.props.suggestionsListId}
/>
<AccessibleButton
onClick={this.onItemAdded}
kind="primary"
type="submit"
disabled={!this.props.newItem}
>
{ _t("Add") }
</AccessibleButton>
</form>
);
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
key={item}
index={index}
value={item}
onRemove={this._onItemRemoved}
onRemove={this.onItemRemoved}
/>;
});
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
return (
<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItemsSection }
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
</div>
{ editableItemsSection }
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
</div>);
);
}
}

View file

@ -102,7 +102,8 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
// Fake it more
event.sender = {
name: this.props.displayName,
name: this.props.displayName || this.props.userId,
rawDisplayName: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(

View file

@ -29,6 +29,11 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
export interface IValidateOpts {
focused?: boolean;
allowEmpty?: boolean;
}
interface IProps {
// The field's ID, which binds the input and label together. Immutable.
id?: string;
@ -180,7 +185,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
}
};
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
public async validate({ focused, allowEmpty = true }: IValidateOpts) {
if (!this.props.onValidate) {
return;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -14,38 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import React from "react";
import ToggleSwitch from "./ToggleSwitch";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
// The value for the toggle switch
value: boolean;
// The translated label for the switch
label: string;
// Whether or not to disable the toggle switch
disabled?: boolean;
// True to put the toggle in front of the label
// Default false.
toggleInFront?: boolean;
// Additional class names to append to the switch. Optional.
className?: string;
// The function to call when the value changes
onChange(checked: boolean): void;
}
@replaceableComponent("views.elements.LabelledToggleSwitch")
export default class LabelledToggleSwitch extends React.Component {
static propTypes = {
// The value for the toggle switch
value: PropTypes.bool.isRequired,
// The function to call when the value changes
onChange: PropTypes.func.isRequired,
// The translated label for the switch
label: PropTypes.string.isRequired,
// Whether or not to disable the toggle switch
disabled: PropTypes.bool,
// True to put the toggle in front of the label
// Default false.
toggleInFront: PropTypes.bool,
// Additional class names to append to the switch. Optional.
className: PropTypes.string,
};
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
render() {
// This is a minimal version of a SettingsFlag
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
let firstPart = <span className="mx_SettingsFlag_label">{ this.props.label }</span>;
let secondPart = <ToggleSwitch
checked={this.props.value}
disabled={this.props.disabled}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
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.
@ -13,67 +13,78 @@ 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 { _t } from '../../../languageHandler';
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import withValidation from './Validation';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Field, { IValidateOpts } from "./Field";
interface IProps {
domain: string;
value: string;
label?: string;
placeholder?: string;
onChange?(value: string): void;
}
interface IState {
isValid: boolean;
}
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
@replaceableComponent("views.elements.RoomAliasField")
export default class RoomAliasField extends React.PureComponent {
static propTypes = {
domain: PropTypes.string.isRequired,
onChange: PropTypes.func,
value: PropTypes.string.isRequired,
};
export default class RoomAliasField extends React.PureComponent<IProps, IState> {
private fieldRef = createRef<Field>();
constructor(props) {
super(props);
this.state = {isValid: true};
constructor(props, context) {
super(props, context);
this.state = {
isValid: true,
};
}
_asFullAlias(localpart) {
private asFullAlias(localpart: string): string {
return `#${localpart}:${this.props.domain}`;
}
render() {
const Field = sdk.getComponent('views.elements.Field');
const poundSign = (<span>#</span>);
const aliasPostfix = ":" + this.props.domain;
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
<Field
label={_t("Room address")}
label={this.props.label || _t("Room address")}
className="mx_RoomAliasField"
prefixComponent={poundSign}
postfixComponent={domain}
ref={ref => this._fieldRef = ref}
onValidate={this._onValidate}
placeholder={_t("e.g. my-room")}
onChange={this._onChange}
ref={this.fieldRef}
onValidate={this.onValidate}
placeholder={this.props.placeholder || _t("e.g. my-room")}
onChange={this.onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength}
/>
);
}
_onChange = (ev) => {
private onChange = (ev) => {
if (this.props.onChange) {
this.props.onChange(this._asFullAlias(ev.target.value));
this.props.onChange(this.asFullAlias(ev.target.value));
}
};
_onValidate = async (fieldState) => {
const result = await this._validationRules(fieldState);
private onValidate = async (fieldState) => {
const result = await this.validationRules(fieldState);
this.setState({isValid: result.valid});
return result;
};
_validationRules = withValidation({
private validationRules = withValidation({
rules: [
{
key: "safeLocalpart",
@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
if (!value) {
return true;
}
const fullAlias = this._asFullAlias(value);
const fullAlias = this.asFullAlias(value);
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Please provide a room address"),
invalid: () => _t("Please provide an address"),
}, {
key: "taken",
final: true,
@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
}
const client = MatrixClientPeg.get();
try {
await client.getRoomIdForAlias(this._asFullAlias(value));
await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
],
});
get isValid() {
public get isValid() {
return this.state.isValid;
}
validate(options) {
return this._fieldRef.validate(options);
public validate(options: IValidateOpts) {
return this.fieldRef.current?.validate(options);
}
focus() {
this._fieldRef.focus();
public focus() {
this.fieldRef.current?.focus();
}
}

View file

@ -34,10 +34,19 @@ interface IProps<T extends string> {
definitions: IDefinition<T>[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
disabled?: boolean;
onChange(newValue: T): void;
}
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
function StyledRadioGroup<T extends string>({
name,
definitions,
value,
className,
outlined,
disabled,
onChange,
}: IProps<T>) {
const _onChange = e => {
onChange(e.target.value);
};
@ -50,12 +59,12 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
disabled={d.disabled}
disabled={disabled || d.disabled}
outlined={outlined}
>
{d.label}
{ d.label }
</StyledRadioButton>
{d.description}
{ d.description ? <span>{ d.description }</span> : null }
</React.Fragment>)}
</React.Fragment>;
}

View file

@ -16,31 +16,29 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TruncatedList")
export default class TruncatedList extends React.Component {
static propTypes = {
// The number of elements to show before truncating. If negative, no truncation is done.
truncateAt: PropTypes.number,
// The className to apply to the wrapping div
className: PropTypes.string,
// A function that returns the children to be rendered into the element.
// function getChildren(start: number, end: number): Array<React.Node>
// The start element is included, the end is not (as in `slice`).
// If omitted, the React child elements will be used. This parameter can be used
// to avoid creating unnecessary React elements.
getChildren: PropTypes.func,
// A function that should return the total number of child element available.
// Required if getChildren is supplied.
getChildCount: PropTypes.func,
// A function which will be invoked when an overflow element is required.
// This will be inserted after the children.
createOverflowElement: PropTypes.func,
};
interface IProps {
// The number of elements to show before truncating. If negative, no truncation is done.
truncateAt?: number;
// The className to apply to the wrapping div
className?: string;
// A function that returns the children to be rendered into the element.
// The start element is included, the end is not (as in `slice`).
// If omitted, the React child elements will be used. This parameter can be used
// to avoid creating unnecessary React elements.
getChildren?: (start: number, end: number) => Array<React.ReactNode>;
// A function that should return the total number of child element available.
// Required if getChildren is supplied.
getChildCount?: () => number;
// A function which will be invoked when an overflow element is required.
// This will be inserted after the children.
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
}
@replaceableComponent("views.elements.TruncatedList")
export default class TruncatedList extends React.Component<IProps> {
static defaultProps ={
truncateAt: 2,
createOverflowElement(overflowCount, totalCount) {
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
},
};
_getChildren(start, end) {
private getChildren(start: number, end: number): Array<React.ReactNode> {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildren(start, end);
} else {
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
}
}
_getChildCount() {
private getChildCount(): number {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildCount();
} else {
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
}
}
render() {
public render() {
let overflowNode = null;
const totalChildren = this._getChildCount();
const totalChildren = this.getChildCount();
let upperBound = totalChildren;
if (this.props.truncateAt >= 0) {
const overflowCount = totalChildren - this.props.truncateAt;
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
upperBound = this.props.truncateAt;
}
}
const childNodes = this._getChildren(0, upperBound);
const childNodes = this.getChildren(0, upperBound);
return (
<div className={this.props.className}>