@@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
@@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
@@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
return (
);
}
}
+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 extends React.PureComponent {
+ 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 (
);
@@ -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 : ;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
- return (
-
- { label }
+ return (
+
+
+ { label }
+
+ { editableItemsSection }
+ { this.props.canEdit ? this.renderNewItemField() :
}
- { editableItemsSection }
- { this.props.canEdit ? this._renderNewItemField() :
}
-
);
+ );
}
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index cf3b7a6e61..332f3ac333 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { Layout } from "../../../settings/Layout";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/**
@@ -101,7 +102,8 @@ export default class EventTilePreview extends React.Component
{
// 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(
@@ -110,7 +112,7 @@ export default class EventTilePreview extends React.Component {
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
- };
+ } as RoomMember;
return event;
}
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 59d9a11596..1373c2df0e 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -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 {
}
};
- public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
+ public async validate({ focused, allowEmpty = true }: IValidateOpts) {
if (!this.props.onValidate) {
return;
}
diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js
deleted file mode 100644
index f6b4c986f5..0000000000
--- a/src/components/views/elements/FormButton.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-Copyright 2019 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 from 'react';
-import AccessibleButton from "./AccessibleButton";
-
-export default function FormButton(props) {
- const {className, label, kind, ...restProps} = props;
- const newClassName = (className || "") + " mx_FormButton";
- const allProps = Object.assign({}, restProps,
- {className: newClassName, kind: kind || "primary", children: [label]});
- return React.createElement(AccessibleButton, allProps);
-}
-
-FormButton.propTypes = AccessibleButton.propTypes;
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx
similarity index 63%
rename from src/components/views/elements/LabelledToggleSwitch.js
rename to src/components/views/elements/LabelledToggleSwitch.tsx
index ef60eeed7b..d97b698fd8 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -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 {
render() {
// This is a minimal version of a SettingsFlag
- let firstPart = {this.props.label} ;
+ let firstPart = { this.props.label } ;
let secondPart = {
return res;
}
- private static getTransitionSequence(events: MatrixEvent[]) {
+ private static getTransitionSequence(events: IUserEvents[]) {
return events.map(MemberEventListSummary.getTransition);
}
@@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component {
* @returns {string?} the transition type given to this event. This defaults to `null`
* if a transition is not recognised.
*/
- private static getTransition(e: MatrixEvent): TransitionType {
+ private static getTransition(e: IUserEvents): TransitionType {
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
// Handle 3pid invites the same as invites so they get bundled together
if (!isValid3pidInvite(e.mxEvent)) {
diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js
index 81ed360b17..ebf4a18aa9 100644
--- a/src/components/views/elements/ReplyThread.js
+++ b/src/components/views/elements/ReplyThread.js
@@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component {
}
async getEvent(eventId) {
+ if (!eventId) return null;
const event = this.room.findEventById(eventId);
if (event) return event;
@@ -392,6 +393,7 @@ export default class ReplyThread extends React.Component {
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
replacingEventId={ev.replacingEventId()}
+ as="div"
/>
;
});
diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.tsx
similarity index 65%
rename from src/components/views/elements/RoomAliasField.js
rename to src/components/views/elements/RoomAliasField.tsx
index 813dd8b5cc..74af311b47 100644
--- a/src/components/views/elements/RoomAliasField.js
+++ b/src/components/views/elements/RoomAliasField.tsx
@@ -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 {
+ private fieldRef = createRef();
- 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 = (# );
const aliasPostfix = ":" + this.props.domain;
const domain = ({aliasPostfix} );
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
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();
}
}
diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx
index 9178155d19..cdd83aedc2 100644
--- a/src/components/views/elements/RoomName.tsx
+++ b/src/components/views/elements/RoomName.tsx
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {useEffect, useState} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
+import React, { useEffect, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
interface IProps {
room: Room;
@@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
}, [room]);
if (children) return children(name);
- return name || "";
+ return <>{ name || "" }>;
};
export default RoomName;
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 4f885ab47d..24a21e1a33 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component {
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
- let label = this.props.label;
- if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
- else label = _t(label);
+ const label = this.props.label
+ ? _t(this.props.label)
+ : SettingsStore.getDisplayName(this.props.name, this.props.level);
+ const description = SettingsStore.getDescription(this.props.name);
if (this.props.useCheckbox) {
return {
disabled={this.props.disabled || !canChange}
aria-label={label}
/>
+ { description &&
+ { description }
+
}
);
}
diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx
index 6b9e992f92..744b6f2059 100644
--- a/src/components/views/elements/StyledRadioGroup.tsx
+++ b/src/components/views/elements/StyledRadioGroup.tsx
@@ -34,10 +34,19 @@ interface IProps {
definitions: IDefinition[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
+ disabled?: boolean;
onChange(newValue: T): void;
}
-function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) {
+function StyledRadioGroup({
+ name,
+ definitions,
+ value,
+ className,
+ outlined,
+ disabled,
+ onChange,
+}: IProps) {
const _onChange = e => {
onChange(e.target.value);
};
@@ -50,12 +59,12 @@ function StyledRadioGroup({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 }
- {d.description}
+ { d.description ? { d.description } : null }
)}
;
}
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.tsx
similarity index 65%
rename from src/components/views/elements/TruncatedList.js
rename to src/components/views/elements/TruncatedList.tsx
index 0509775545..395caa9222 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.tsx
@@ -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
- // 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;
+ // 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 {
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 {
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 (
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index c06d827550..6bef141cb8 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
return
-
+
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index 42a977fb79..dd8366bbe0 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import { Draggable, Droppable } from 'react-beautiful-dnd';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
-
-function nop() {}
+import { _t } from "../../../languageHandler";
+import TagOrderActions from "../../../actions/TagOrderActions";
+import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.groups.GroupTile")
class GroupTile extends React.Component {
@@ -34,7 +34,6 @@ class GroupTile extends React.Component {
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
- draggable: PropTypes.bool,
};
static contextType = MatrixClientContext;
@@ -42,7 +41,6 @@ class GroupTile extends React.Component {
static defaultProps = {
showDescription: true,
avatarHeight: 50,
- draggable: true,
};
state = {
@@ -57,7 +55,7 @@ class GroupTile extends React.Component {
});
}
- onMouseDown = e => {
+ onClick = e => {
e.preventDefault();
dis.dispatch({
action: 'view_group',
@@ -65,6 +63,18 @@ class GroupTile extends React.Component {
});
};
+ onPinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
+ };
+
+ onUnpinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
+ };
+
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@@ -78,7 +88,7 @@ class GroupTile extends React.Component {
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
: null;
- let avatarElement = (
+ const avatarElement = (
);
- if (this.props.draggable) {
- const avatarClone = avatarElement;
- avatarElement = (
-
- { (droppableProvided, droppableSnapshot) => (
-
-
- { (provided, snapshot) => (
-
-
- {avatarClone}
-
- { /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
- { provided.placeholder ? avatarClone :
}
-
- ) }
-
-
- ) }
-
- );
- }
- // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
- // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
- return
+ return
{ avatarElement }
{ name }
{ descElement }
{ this.props.groupId }
+ { !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
+ ?
+ { _t("Pin") }
+
+ :
+ { _t("Unpin") }
+
+ }
;
}
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.tsx
similarity index 76%
rename from src/components/views/messages/MKeyVerificationRequest.js
rename to src/components/views/messages/MKeyVerificationRequest.tsx
index 988606a766..69467cfa50 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.tsx
@@ -15,41 +15,40 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixEvent } from 'matrix-js-sdk/src';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
-import {getNameForEventRoom, userLabelForEventRoom}
+import { getNameForEventRoom, userLabelForEventRoom }
from '../../../utils/KeyVerificationStateObserver';
import dis from "../../../dispatcher/dispatcher";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {Action} from "../../../dispatcher/actions";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { Action } from "../../../dispatcher/actions";
import EventTileBubble from "./EventTileBubble";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ mxEvent: MatrixEvent
+}
@replaceableComponent("views.messages.MKeyVerificationRequest")
-export default class MKeyVerificationRequest extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- componentDidMount() {
+export default class MKeyVerificationRequest extends React.Component {
+ public componentDidMount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
- request.on("change", this._onRequestChanged);
+ request.on("change", this.onRequestChanged);
}
}
- componentWillUnmount() {
+ public componentWillUnmount() {
const request = this.props.mxEvent.verificationRequest;
if (request) {
- request.off("change", this._onRequestChanged);
+ request.off("change", this.onRequestChanged);
}
}
- _openRequest = () => {
- const {verificationRequest} = this.props.mxEvent;
+ private openRequest = () => {
+ const { verificationRequest } = this.props.mxEvent;
const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId);
dis.dispatch({
action: Action.SetRightPanelPhase,
@@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component {
});
};
- _onRequestChanged = () => {
+ private onRequestChanged = () => {
this.forceUpdate();
};
- _onAcceptClicked = async () => {
+ private onAcceptClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
- this._openRequest();
+ this.openRequest();
await request.accept();
} catch (err) {
console.error(err.message);
@@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
- _onRejectClicked = async () => {
+ private onRejectClicked = async () => {
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
};
- _acceptedLabel(userId) {
+ private acceptedLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
@@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component {
}
}
- _cancelledLabel(userId) {
+ private cancelledLabel(userId: string) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const {cancellationCode} = this.props.mxEvent.verificationRequest;
@@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component {
}
}
- render() {
+ public render() {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
- const FormButton = sdk.getComponent("elements.FormButton");
const {mxEvent} = this.props;
const request = mxEvent.verificationRequest;
@@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component {
let stateLabel;
const accepted = request.ready || request.started || request.done;
if (accepted) {
- stateLabel = (
- {this._acceptedLabel(request.receivingUserId)}
+ stateLabel = (
+ {this.acceptedLabel(request.receivingUserId)}
);
} else if (request.cancelled) {
- stateLabel = this._cancelledLabel(request.cancellingUserId);
+ stateLabel = this.cancelledLabel(request.cancellingUserId);
} else if (request.accepting) {
stateLabel = _t("Accepting …");
} else if (request.declining) {
@@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component {
subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId());
if (request.canAccept) {
stateNode = (
-
-
+
+ {_t("Decline")}
+
+
+ {_t("Accept")}
+
);
}
} else { // request sent by us
@@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component {
return null;
}
}
-
-MKeyVerificationRequest.propTypes = {
- /* the MatrixEvent to show */
- mxEvent: PropTypes.object.isRequired,
-};
diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx
index d65de7697a..a7e3b1cd86 100644
--- a/src/components/views/messages/MVoiceMessageBody.tsx
+++ b/src/components/views/messages/MVoiceMessageBody.tsx
@@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler";
import {mediaFromContent} from "../../../customisations/Media";
import {decryptFile} from "../../../utils/DecryptFile";
import RecordingPlayback from "../voice_messages/RecordingPlayback";
+import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
interface IProps {
mxEvent: MatrixEvent;
@@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent {
public render() {
- const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
+ // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
+ const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
+ || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
if (isVoiceMessage && voiceMessagesEnabled) {
return ;
diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.tsx
similarity index 67%
rename from src/components/views/messages/MessageTimestamp.js
rename to src/components/views/messages/MessageTimestamp.tsx
index a7f350adcd..8b02f6b38e 100644
--- a/src/components/views/messages/MessageTimestamp.js
+++ b/src/components/views/messages/MessageTimestamp.tsx
@@ -16,20 +16,19 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ ts: number;
+ showTwelveHour?: boolean;
+ showFullDate?: boolean;
+ showSeconds?: boolean;
+}
@replaceableComponent("views.messages.MessageTimestamp")
-export default class MessageTimestamp extends React.Component {
- static propTypes = {
- ts: PropTypes.number.isRequired,
- showTwelveHour: PropTypes.bool,
- showFullDate: PropTypes.bool,
- showSeconds: PropTypes.bool,
- };
-
- render() {
+export default class MessageTimestamp extends React.Component {
+ public render() {
const date = new Date(this.props.ts);
let timestamp;
if (this.props.showFullDate) {
@@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
}
return (
-
+
{timestamp}
);
diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index de1549dffa..e604b04ab0 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -17,10 +17,10 @@
import React from 'react';
import Flair from '../elements/Flair';
import FlairStore from '../../../stores/FlairStore';
-import {getUserNameColorClass} from '../../../utils/FormattingUtils';
+import { getUserNameColorClass } from '../../../utils/FormattingUtils';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-import MatrixEvent from "matrix-js-sdk/src/models/event";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
interface IProps {
mxEvent: MatrixEvent;
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 3adfea6ee6..ebc4ce7ce8 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -16,12 +16,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
+import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
-import {formatDate} from '../../../DateUtils';
+import { formatDate } from '../../../DateUtils';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
@@ -29,14 +29,16 @@ import { _t } from '../../../languageHandler';
import * as ContextMenu from '../../structures/ContextMenu';
import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
-import {pillifyLinks, unmountPills} from '../../../utils/pillify';
-import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
-import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
-import {toRightOf} from "../../structures/ContextMenu";
-import {copyPlaintext} from "../../../utils/strings";
+import { pillifyLinks, unmountPills } from '../../../utils/pillify';
+import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
+import { isPermalinkHost } from "../../../utils/permalinks/Permalinks";
+import { toRightOf } from "../../structures/ContextMenu";
+import { copyPlaintext } from "../../../utils/strings";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import UIStore from "../../../stores/UIStore";
+import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
+import { Action } from "../../../dispatcher/actions";
@replaceableComponent("views.messages.TextualBody")
export default class TextualBody extends React.Component {
@@ -390,9 +392,9 @@ export default class TextualBody extends React.Component {
onEmoteSenderClick = event => {
const mxEvent = this.props.mxEvent;
- dis.dispatch({
- action: 'insert_mention',
- user_id: mxEvent.getSender(),
+ dis.dispatch({
+ action: Action.ComposerInsert,
+ userId: mxEvent.getSender(),
});
};
diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx
index aa51965ac6..db59a88967 100644
--- a/src/components/views/right_panel/EncryptionInfo.tsx
+++ b/src/components/views/right_panel/EncryptionInfo.tsx
@@ -17,8 +17,9 @@ limitations under the License.
import React from "react";
import * as sdk from "../../../index";
-import {_t} from "../../../languageHandler";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { _t } from "../../../languageHandler";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
export const PendingActionSpinner = ({text}) => {
const Spinner = sdk.getComponent('elements.Spinner');
@@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => {
interface IProps {
waitingForOtherParty: boolean;
waitingForNetwork: boolean;
- member: RoomMember;
+ member: RoomMember | User;
onStartVerification: () => Promise;
isRoomEncrypted: boolean;
inDialog: boolean;
@@ -55,7 +56,7 @@ const EncryptionInfo: React.FC = ({
text = _t("Accept on your other login…");
} else {
text = _t("Waiting for %(displayName)s to accept…", {
- displayName: member.displayName || member.name || member.userId,
+ displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
});
}
} else {
diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx
index c237a4ade6..3a26427246 100644
--- a/src/components/views/right_panel/EncryptionPanel.tsx
+++ b/src/components/views/right_panel/EncryptionPanel.tsx
@@ -14,28 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useCallback, useEffect, useState} from "react";
+import React, { useCallback, useEffect, useState } from "react";
+import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
+import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import EncryptionInfo from "./EncryptionInfo";
import VerificationPanel from "./VerificationPanel";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import {ensureDMExists} from "../../../createRoom";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { ensureDMExists } from "../../../createRoom";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
import Modal from "../../../Modal";
-import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
-import {_t} from "../../../languageHandler";
-import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
-import {Action} from "../../../dispatcher/actions";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
+import { Action } from "../../../dispatcher/actions";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
interface IProps {
- member: RoomMember;
+ member: RoomMember | User;
onClose: () => void;
verificationRequest: VerificationRequest;
verificationRequestPromise: Promise;
diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index a72731522f..1131c02dbf 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, {useCallback, useContext, useEffect, useState} from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event';
@@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
import PinningUtils from "../../../utils/PinningUtils";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import PinnedEventTile from "../rooms/PinnedEventTile";
+import { useRoomState } from "../../../hooks/useRoomState";
interface IProps {
room: Room;
@@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set => {
return readPinnedEvents;
};
-const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => {
- const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
-
- const update = useCallback(() => {
- if (!room) return;
- setValue(mapper(room.currentState));
- }, [room, mapper]);
-
- useEventEmitter(room?.currentState, "RoomState.events", update);
- useEffect(() => {
- update();
- return () => {
- setValue(undefined);
- };
- }, [update]);
- return value;
-};
-
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
const cli = useContext(MatrixClientContext);
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index d6c97f9cf2..e22b2d5ff4 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -38,7 +38,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { textualPowerLevel } from '../../../Roles';
@@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
import { Action } from "../../../dispatcher/actions";
-import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
+import { UserTab } from "../dialogs/UserSettingsDialog";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import { E2EStatus } from "../../../utils/ShieldUtils";
@@ -68,6 +68,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
+import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
export interface IDevice {
deviceId: string;
@@ -146,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) {
type SetUpdating = (updating: boolean) => void;
-function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
+function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) {
return useAsyncMemo(async () => {
if (!canVerify) {
return undefined;
@@ -368,9 +369,9 @@ const UserOptionsSection: React.FC<{
};
const onInsertPillButton = function() {
- dis.dispatch({
- action: 'insert_mention',
- user_id: member.userId,
+ dis.dispatch({
+ action: Action.ComposerInsert,
+ userId: member.userId,
});
};
@@ -502,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
return member.powerLevel < levelToSend;
};
+const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
+
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
- const [powerLevels, setPowerLevels] = useState({});
+ const [powerLevels, setPowerLevels] = useState(getPowerLevels(room));
const update = useCallback((ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
-
- const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- if (event) {
- setPowerLevels(event.getContent());
- } else {
- setPowerLevels({});
- }
+ setPowerLevels(getPowerLevels(room));
}, [room]);
useEventEmitter(cli, "RoomState.events", update);
@@ -971,7 +968,7 @@ interface IRoomPermissions {
canInvite: boolean;
}
-function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
+function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
const [roomPermissions, setRoomPermissions] = useState({
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
modifyLevelMax: -1,
@@ -1028,7 +1025,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer
}
const PowerLevelSection: React.FC<{
- user: User;
+ user: RoomMember;
room: Room;
roomPermissions: IRoomPermissions;
powerLevels: IPowerLevelsContent;
@@ -1037,7 +1034,7 @@ const PowerLevelSection: React.FC<{
return ( );
} else {
const powerLevelUsersDefault = powerLevels.users_default || 0;
- const powerLevel = parseInt(user.powerLevel, 10);
+ const powerLevel = user.powerLevel;
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
return (
@@ -1048,13 +1045,13 @@ const PowerLevelSection: React.FC<{
};
const PowerLevelEditor: React.FC<{
- user: User;
+ user: RoomMember;
room: Room;
roomPermissions: IRoomPermissions;
}> = ({user, room, roomPermissions}) => {
const cli = useContext(MatrixClientContext);
- const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
+ const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
const onPowerChange = useCallback(async (powerLevelStr: string) => {
const powerLevel = parseInt(powerLevelStr, 10);
setSelectedPowerLevel(powerLevel);
@@ -1231,7 +1228,7 @@ const BasicUserInfo: React.FC<{
setPendingUpdateCount(pendingUpdateCount - 1);
}, [pendingUpdateCount]);
- const roomPermissions = useRoomPermissions(cli, room, member);
+ const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
const onSynapseDeactivate = useCallback(async () => {
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
@@ -1275,12 +1272,26 @@ const BasicUserInfo: React.FC<{
);
}
+ let memberDetails;
let adminToolsContainer;
- if (room && member.roomId) {
+ if (room && (member as RoomMember).roomId) {
+ // hide the Roles section for DMs as it doesn't make sense there
+ if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
+ memberDetails =
;
+ }
+
adminToolsContainer = (
@@ -1309,20 +1320,6 @@ const BasicUserInfo: React.FC<{
spinner = ;
}
- let memberDetails;
- // hide the Roles section for DMs as it doesn't make sense there
- if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) {
- memberDetails = ;
- }
-
// only display the devices list if our client supports E2E
const cryptoEnabled = cli.isCryptoEnabled();
@@ -1349,8 +1346,7 @@ const BasicUserInfo: React.FC<{
const setUpdating = (updating) => {
setPendingUpdateCount(count => count + (updating ? 1 : -1));
};
- const hasCrossSigningKeys =
- useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
+ const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
const showDeviceListSpinner = devices === undefined;
if (canVerify) {
@@ -1359,9 +1355,9 @@ const BasicUserInfo: React.FC<{
verifyButton = (
{
if (hasCrossSigningKeys) {
- verifyUser(member);
+ verifyUser(member as User);
} else {
- legacyVerifyUser(member);
+ legacyVerifyUser(member as User);
}
}}>
{_t("Verify")}
@@ -1381,7 +1377,7 @@ const BasicUserInfo: React.FC<{
{
dis.dispatch({
action: Action.ViewUserSettings,
- initialTabId: USER_SECURITY_TAB,
+ initialTabId: UserTab.Security,
});
}}>
{ _t("Edit devices") }
@@ -1409,7 +1405,7 @@ const BasicUserInfo: React.FC<{
@@ -1428,13 +1424,15 @@ const UserInfoHeader: React.FC<{
const cli = useContext(MatrixClientContext);
const onMemberAvatarClick = useCallback(() => {
- const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
+ const avatarUrl = (member as RoomMember).getMxcAvatarUrl
+ ? (member as RoomMember).getMxcAvatarUrl()
+ : (member as User).avatarUrl;
if (!avatarUrl) return;
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
const params = {
src: httpUrl,
- name: member.name,
+ name: (member as RoomMember).name || (member as User).displayName,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
@@ -1446,13 +1444,13 @@ const UserInfoHeader: React.FC<{
+ urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
@@ -1469,7 +1467,11 @@ const UserInfoHeader: React.FC<{
presenceCurrentlyActive = member.user.currentlyActive;
if (SettingsStore.getValue("feature_custom_status")) {
- statusMessage = member.user._unstable_statusMessage;
+ if ((member as RoomMember).user) {
+ statusMessage = member.user.unstable_statusMessage;
+ } else {
+ statusMessage = (member as unknown as User).unstable_statusMessage;
+ }
}
}
@@ -1500,7 +1502,7 @@ const UserInfoHeader: React.FC<{
e2eIcon =
;
}
- const displayName = member.rawDisplayName || member.displayname;
+ const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
return
{ avatarElement }
@@ -1588,7 +1590,7 @@ const UserInfo: React.FC = ({
content = (
@@ -1599,7 +1601,7 @@ const UserInfo: React.FC = ({
content = (
}
- member={member}
+ member={member as User | RoomMember}
onClose={onEncryptionPanelClose}
isRoomEncrypted={isRoomEncrypted}
/>
diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx
index ac01c953b9..d3f2ba8cbf 100644
--- a/src/components/views/right_panel/VerificationPanel.tsx
+++ b/src/components/views/right_panel/VerificationPanel.tsx
@@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
@@ -51,7 +52,7 @@ enum VerificationPhase {
interface IProps {
layout: string;
request: VerificationRequest;
- member: RoomMember;
+ member: RoomMember | User;
phase: VerificationPhase;
onClose: () => void;
isRoomEncrypted: boolean;
@@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent
{_t("Verify by scanning")}
{_t("Ask %(displayName)s to scan your code:", {
- displayName: member.displayName || member.name || member.userId,
+ displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
})}
@@ -194,37 +195,33 @@ export default class VerificationPanel extends React.PureComponent
{description}
-
{_t("No")}
-
+ { _t("No") }
+
+ {_t("Yes")}
+ onClick={this.onReciprocateYesClick}
+ >
+ { _t("Yes") }
+
;
} else {
@@ -264,7 +261,7 @@ export default class VerificationPanel extends React.PureComponent {
+ private aliasField = createRef();
- _onAliasAdded = async () => {
- await this._aliasField.current.validate({ allowEmpty: false });
+ private onAliasAdded = async () => {
+ await this.aliasField.current.validate({ allowEmpty: false });
- if (this._aliasField.current.isValid) {
+ if (this.aliasField.current.isValid) {
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
return;
}
- this._aliasField.current.focus();
- this._aliasField.current.validate({ allowEmpty: false, focused: true });
+ this.aliasField.current.focus();
+ this.aliasField.current.validate({ allowEmpty: false, focused: true });
};
- _renderNewItemField() {
+ protected renderNewItemField() {
// if we don't need the RoomAliasField,
- // we don't need to overriden version of _renderNewItemField
+ // we don't need to overriden version of renderNewItemField
if (!this.props.domain) {
- return super._renderNewItemField();
+ return super.renderNewItemField();
}
- const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
- const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
+ const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
return (
@@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
}
}
-@replaceableComponent("views.room_settings.AliasSettings")
-export default class AliasSettings extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- canSetCanonicalAlias: PropTypes.bool.isRequired,
- canSetAliases: PropTypes.bool.isRequired,
- canonicalAliasEvent: PropTypes.object, // MatrixEvent
- };
+interface IProps {
+ roomId: string;
+ canSetCanonicalAlias: boolean;
+ canSetAliases: boolean;
+ canonicalAliasEvent?: MatrixEvent;
+ hidePublishSetting?: boolean;
+}
+interface IState {
+ altAliases: string[];
+ localAliases: string[];
+ canonicalAlias?: string;
+ updatingCanonicalAlias: boolean;
+ localAliasesLoading: boolean;
+ detailsOpen: boolean;
+ newAlias?: string;
+ newAltAlias?: string;
+}
+
+@replaceableComponent("views.room_settings.AliasSettings")
+export default class AliasSettings extends React.Component {
static defaultProps = {
canSetAliases: false,
canSetCanonicalAlias: false,
- aliasEvents: [],
};
constructor(props) {
@@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
}
}
- async loadLocalAliases() {
+ private async loadLocalAliases() {
this.setState({ localAliasesLoading: true });
try {
const cli = MatrixClientPeg.get();
@@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component {
}
}
this.setState({ localAliases });
+
+ if (localAliases.length === 0) {
+ this.setState({ detailsOpen: true });
+ }
} finally {
this.setState({ localAliasesLoading: false });
}
}
- changeCanonicalAlias(alias) {
+ private changeCanonicalAlias(alias: string) {
if (!this.props.canSetCanonicalAlias) return;
const oldAlias = this.state.canonicalAlias;
@@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component {
});
}
- changeAltAliases(altAliases) {
+ private changeAltAliases(altAliases: string[]) {
if (!this.props.canSetCanonicalAlias) return;
this.setState({
@@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component {
const eventContent = {};
if (this.state.canonicalAlias) {
- eventContent.alias = this.state.canonicalAlias;
+ eventContent["alias"] = this.state.canonicalAlias;
}
if (altAliases) {
eventContent["alt_aliases"] = altAliases;
@@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component {
});
}
- onNewAliasChanged = (value) => {
- this.setState({newAlias: value});
+ private onNewAliasChanged = (value: string) => {
+ this.setState({ newAlias: value });
};
- onLocalAliasAdded = (alias) => {
+ private onLocalAliasAdded = (alias: string) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
const localDomain = MatrixClientPeg.get().getDomain();
@@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasDeleted = (index) => {
+ private onLocalAliasDeleted = (index: number) => {
const alias = this.state.localAliases[index];
// TODO: In future, we should probably be making sure that the alias actually belongs
// to this room. See https://github.com/vector-im/element-web/issues/7353
@@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasesToggled = (event) => {
+ private onLocalAliasesToggled = (event: ChangeEvent) => {
// expanded
if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount
@@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component {
this.loadLocalAliases();
}
}
- this.setState({detailsOpen: event.target.open});
+ this.setState({ detailsOpen: event.currentTarget.open });
};
- onCanonicalAliasChange = (event) => {
+ private onCanonicalAliasChange = (event: ChangeEvent) => {
this.changeCanonicalAlias(event.target.value);
};
- onNewAltAliasChanged = (value) => {
- this.setState({newAltAlias: value});
+ private onNewAltAliasChanged = (value: string) => {
+ this.setState({ newAltAlias: value });
}
- onAltAliasAdded = (alias) => {
+ private onAltAliasAdded = (alias: string) => {
const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim());
this.changeAltAliases(altAliases);
- this.setState({newAltAlias: ""});
+ this.setState({ newAltAlias: "" });
}
}
- onAltAliasDeleted = (index) => {
+ private onAltAliasDeleted = (index: number) => {
const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1);
this.changeAltAliases(altAliases);
}
- _getAliases() {
- return this.state.altAliases.concat(this._getLocalNonAltAliases());
+ private getAliases() {
+ return this.state.altAliases.concat(this.getLocalNonAltAliases());
}
- _getLocalNonAltAliases() {
+ private getLocalNonAltAliases() {
const {altAliases} = this.state;
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
}
render() {
- const localDomain = MatrixClientPeg.get().getDomain();
+ const cli = MatrixClientPeg.get();
+ const localDomain = cli.getDomain();
+ const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
@@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component {
>
{ _t('not specified') }
{
- this._getAliases().map((alias, i) => {
+ this.getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true;
return (
@@ -340,12 +357,10 @@ export default class AliasSettings extends React.Component {
let localAliasesList;
if (this.state.localAliasesLoading) {
- const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = ;
} else {
localAliasesList = ( );
@@ -362,18 +379,27 @@ export default class AliasSettings extends React.Component {
return (
{_t("Published Addresses")}
-
{_t("Published addresses can be used by anyone on any server to join your room. " +
- "To publish an address, it needs to be set as a local address first.")}
- {canonicalAliasSection}
-
+
+ { isSpaceRoom
+ ? _t("Published addresses can be used by anyone on any server to join your space.")
+ : _t("Published addresses can be used by anyone on any server to join your room.")}
+
+ { _t("To publish an address, it needs to be set as a local address first.") }
+
+ { canonicalAliasSection }
+ { this.props.hidePublishSetting
+ ? null
+ :
}
- {this._getLocalNonAltAliases().map(alias => {
+ {this.getLocalNonAltAliases().map(alias => {
return ;
})};
-
{_t("Local Addresses")}
-
{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}
-
+
+ { _t("Local Addresses") }
+
+
+ { isSpaceRoom
+ ? _t("Set addresses for this space so users can find this space " +
+ "through your homeserver (%(localDomain)s)", { localDomain })
+ : _t("Set addresses for this room so users can find this room " +
+ "through your homeserver (%(localDomain)s)", { localDomain }) }
+
+
{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}
- {localAliasesList}
+ { localAliasesList }
);
diff --git a/src/components/views/room_settings/RoomPublishSetting.js b/src/components/views/room_settings/RoomPublishSetting.tsx
similarity index 60%
rename from src/components/views/room_settings/RoomPublishSetting.js
rename to src/components/views/room_settings/RoomPublishSetting.tsx
index 6cc3ce26ba..95b0ac100d 100644
--- a/src/components/views/room_settings/RoomPublishSetting.js
+++ b/src/components/views/room_settings/RoomPublishSetting.tsx
@@ -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.
@@ -14,20 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React from "react";
+
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import {_t} from "../../../languageHandler";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { _t } from "../../../languageHandler";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ label?: string;
+ canSetCanonicalAlias?: boolean;
+}
+
+interface IState {
+ isRoomPublished: boolean;
+}
@replaceableComponent("views.room_settings.RoomPublishSetting")
-export default class RoomPublishSetting extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {isRoomPublished: false};
+export default class RoomPublishSetting extends React.PureComponent {
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isRoomPublished: false,
+ };
}
- onRoomPublishChange = (e) => {
+ private onRoomPublishChange = (e) => {
const valueBefore = this.state.isRoomPublished;
const newValue = !valueBefore;
this.setState({isRoomPublished: newValue});
@@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
render() {
const client = MatrixClientPeg.get();
- return ( );
+ return (
+
+ );
}
}
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index 693ec8bc80..0b32d5d1bb 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -82,13 +82,6 @@ export default class AppsDrawer extends React.Component {
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
}
- // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps(newProps) {
- // Room has changed probably, update apps
- this._updateApps();
- }
-
onIsResizing = (resizing) => {
// This one is the vertical, ie. change height of apps drawer
this.setState({ resizingVertical: resizing });
@@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component {
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
componentDidUpdate(prevProps, prevState) {
- if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
+ if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
+ // Room has changed, update apps
+ this._updateApps();
+ } else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
this._loadResizerPreferences();
}
}
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 6d2ae39059..04c7a57048 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -15,19 +15,20 @@ limitations under the License.
*/
import React from 'react';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import { Room } from 'matrix-js-sdk/src/models/room'
-import dis from "../../../dispatcher/dispatcher";
-import AppsDrawer from './AppsDrawer';
import classNames from 'classnames';
+import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
+import { Room } from 'matrix-js-sdk/src/models/room'
+
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import AppsDrawer from './AppsDrawer';
import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
-import {UIFeature} from "../../../settings/UIFeature";
-import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import { UIFeature } from "../../../settings/UIFeature";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
import CallViewForRoom from '../voip/CallViewForRoom';
-import {objectHasDiff} from "../../../utils/objects";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { objectHasDiff } from "../../../utils/objects";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// js-sdk room object
@@ -69,19 +70,21 @@ export default class AuxPanel extends React.Component {
super(props);
this.state = {
- counters: this._computeCounters(),
+ counters: this.computeCounters(),
};
}
componentDidMount() {
const cli = MatrixClientPeg.get();
- cli.on("RoomState.events", this._rateLimitedUpdate);
+ if (SettingsStore.getValue("feature_state_counters")) {
+ cli.on("RoomState.events", this.rateLimitedUpdate);
+ }
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
- if (cli) {
- cli.removeListener("RoomState.events", this._rateLimitedUpdate);
+ if (cli && SettingsStore.getValue("feature_state_counters")) {
+ cli.removeListener("RoomState.events", this.rateLimitedUpdate);
}
}
@@ -96,30 +99,16 @@ export default class AuxPanel extends React.Component {
}
}
- onConferenceNotificationClick = (ev, type) => {
- dis.dispatch({
- action: 'place_call',
- type: type,
- room_id: this.props.room.roomId,
- });
- ev.stopPropagation();
- ev.preventDefault();
- };
-
- _rateLimitedUpdate = new RateLimitedFunc(() => {
- if (SettingsStore.getValue("feature_state_counters")) {
- this.setState({counters: this._computeCounters()});
- }
+ private rateLimitedUpdate = new RateLimitedFunc(() => {
+ this.setState({ counters: this.computeCounters() });
}, 500);
- _computeCounters() {
+ private computeCounters() {
const counters = [];
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
- stateEvs.sort((a, b) => {
- return a.getStateKey() < b.getStateKey();
- });
+ stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey()));
for (const ev of stateEvs) {
const title = ev.getContent().title;
@@ -225,7 +214,7 @@ export default class AuxPanel extends React.Component {
}
return (
-
+
{ stateViews }
{ appsDrawer }
{ callView }
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index e83f066bd0..981ff1b4ae 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -16,38 +16,39 @@ limitations under the License.
*/
import classNames from 'classnames';
-import React, {createRef, ClipboardEvent} from 'react';
-import {Room} from 'matrix-js-sdk/src/models/room';
+import React, { createRef, ClipboardEvent } from 'react';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
-import {Caret, setSelection} from '../../../editor/caret';
+import { Caret, setSelection } from '../../../editor/caret';
import {
formatRangeAsQuote,
formatRangeAsCode,
toggleInlineFormat,
replaceRangeAndMoveCaret,
} from '../../../editor/operations';
-import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom';
-import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete';
-import {getAutoCompleteCreator} from '../../../editor/parts';
-import {parsePlainTextMessage} from '../../../editor/deserialize';
-import {renderModel} from '../../../editor/render';
+import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
+import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
+import { getAutoCompleteCreator } from '../../../editor/parts';
+import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
+import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
import SettingsStore from "../../../settings/SettingsStore";
-import {Key} from "../../../Keyboard";
-import {EMOTICON_TO_EMOJI} from "../../../emoji";
-import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
+import { Key } from "../../../Keyboard";
+import { EMOTICON_TO_EMOJI } from "../../../emoji";
+import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands";
import Range from "../../../editor/range";
import MessageComposerFormatBar from "./MessageComposerFormatBar";
import DocumentOffset from "../../../editor/offset";
-import {IDiff} from "../../../editor/diff";
+import { IDiff } from "../../../editor/diff";
import AutocompleteWrapperModel from "../../../editor/autocomplete";
import DocumentPosition from "../../../editor/position";
-import {ICompletion} from "../../../autocomplete/Autocompleter";
+import { ICompletion } from "../../../autocomplete/Autocompleter";
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@@ -716,4 +717,48 @@ export default class BasicMessageEditor extends React.Component
focus() {
this.editorRef.current.focus();
}
+
+ public insertMention(userId: string) {
+ const {model} = this.props;
+ const {partCreator} = model;
+ const member = this.props.room.getMember(userId);
+ const displayName = member ?
+ member.rawDisplayName : userId;
+ const caret = this.getCaret();
+ const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
+ // Insert suffix only if the caret is at the start of the composer
+ const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId);
+ model.transform(() => {
+ const addedLen = model.insert(parts, position);
+ return model.positionForOffset(caret.offset + addedLen, true);
+ });
+ // refocus on composer, as we just clicked "Mention"
+ this.focus();
+ }
+
+ public insertQuotedMessage(event: MatrixEvent) {
+ const {model} = this.props;
+ const {partCreator} = model;
+ const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
+ // add two newlines
+ quoteParts.push(partCreator.newline());
+ quoteParts.push(partCreator.newline());
+ model.transform(() => {
+ const addedLen = model.insert(quoteParts, model.positionForOffset(0));
+ return model.positionForOffset(addedLen, true);
+ });
+ // refocus on composer, as we just clicked "Quote"
+ this.focus();
+ }
+
+ public insertPlaintext(text: string) {
+ const {model} = this.props;
+ const {partCreator} = model;
+ const caret = this.getCaret();
+ const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
+ model.transform(() => {
+ const addedLen = model.insert([partCreator.plain(text)], position);
+ return model.positionForOffset(caret.offset + addedLen, true);
+ });
+ }
}
diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js
index f0980af7ae..914f08eac7 100644
--- a/src/components/views/rooms/EditMessageComposer.js
+++ b/src/components/views/rooms/EditMessageComposer.js
@@ -16,25 +16,25 @@ limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
-import {_t, _td} from '../../../languageHandler';
+import { _t, _td } from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import EditorModel from '../../../editor/model';
-import {getCaretOffsetAndText} from '../../../editor/dom';
-import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
-import {findEditableEvent} from '../../../utils/EventUtils';
-import {parseEvent} from '../../../editor/deserialize';
-import {CommandPartCreator} from '../../../editor/parts';
+import { getCaretOffsetAndText } from '../../../editor/dom';
+import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
+import { findEditableEvent } from '../../../utils/EventUtils';
+import { parseEvent } from '../../../editor/deserialize';
+import { CommandPartCreator } from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import classNames from 'classnames';
-import {EventStatus} from 'matrix-js-sdk/src/models/event';
+import { EventStatus } from 'matrix-js-sdk/src/models/event';
import BasicMessageComposer from "./BasicMessageComposer";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {CommandCategories, getCommand} from '../../../SlashCommands';
-import {Action} from "../../../dispatcher/actions";
+import { CommandCategories, getCommand } from '../../../SlashCommands';
+import { Action } from "../../../dispatcher/actions";
import CountlyAnalytics from "../../../CountlyAnalytics";
-import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal';
@@ -124,6 +124,7 @@ export default class EditMessageComposer extends React.Component {
};
this._createEditorModel();
window.addEventListener("beforeunload", this._saveStoredEditorState);
+ this.dispatcherRef = dis.register(this.onAction);
}
_setEditorRef = ref => {
@@ -399,6 +400,7 @@ export default class EditMessageComposer extends React.Component {
if (this._shouldSaveStoredEditorState) {
this._saveStoredEditorState();
}
+ dis.unregister(this.dispatcherRef);
}
_createEditorModel() {
@@ -443,6 +445,18 @@ export default class EditMessageComposer extends React.Component {
});
};
+ onAction = payload => {
+ if (payload.action === "edit_composer_insert" && this._editorRef) {
+ if (payload.userId) {
+ this._editorRef.insertMention(payload.userId);
+ } else if (payload.event) {
+ this._editorRef.insertQuotedMessage(payload.event);
+ } else if (payload.text) {
+ this._editorRef.insertPlaintext(payload.text);
+ }
+ }
+ };
+
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 85b9cac2c4..3d674efe04 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import classNames from "classnames";
-
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
@@ -29,23 +28,25 @@ import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {formatTime} from "../../../DateUtils";
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
+import { Layout } from "../../../settings/Layout";
+import { formatTime } from "../../../DateUtils";
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import { ALL_RULE_TYPES } from "../../../mjolnir/BanList";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import {E2E_STATE} from "./E2EIcon";
-import {toRem} from "../../../utils/units";
-import {WidgetType} from "../../../widgets/WidgetType";
+import { E2E_STATE } from "./E2EIcon";
+import { toRem } from "../../../utils/units";
+import { WidgetType } from "../../../widgets/WidgetType";
import RoomAvatar from "../avatars/RoomAvatar";
-import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
-import {objectHasDiff} from "../../../utils/objects";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { WIDGET_LAYOUT_EVENT_TYPE } from "../../../stores/widgets/WidgetLayoutStore";
+import { objectHasDiff } from "../../../utils/objects";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
-import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
+import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
-import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
+import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge";
+import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
+import { Action } from '../../../dispatcher/actions';
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@@ -376,7 +377,7 @@ export default class EventTile extends React.Component
{
EventType.RoomMessage,
EventType.RoomMessageEncrypted,
];
- if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
+ if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false;
// Default case
return true;
@@ -727,9 +728,9 @@ export default class EventTile extends React.Component {
onSenderProfileClick = event => {
const mxEvent = this.props.mxEvent;
- dis.dispatch({
- action: 'insert_mention',
- user_id: mxEvent.getSender(),
+ dis.dispatch({
+ action: Action.ComposerInsert,
+ userId: mxEvent.getSender(),
});
};
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index 3671069903..f7d562fca0 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import dis from '../../../dispatcher/dispatcher';
import { ActionPayload } from "../../../dispatcher/payloads";
import Stickerpicker from './Stickerpicker';
@@ -28,19 +28,21 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
-import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
+import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
-import {UIFeature} from "../../../settings/UIFeature";
-import {UPDATE_EVENT} from "../../../stores/AsyncStore";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { UIFeature } from "../../../settings/UIFeature";
+import { UPDATE_EVENT } from "../../../stores/AsyncStore";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
-import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
-import {RecordingState} from "../../../voice/VoiceRecording";
-import Tooltip, {Alignment} from "../elements/Tooltip";
+import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
+import { RecordingState } from "../../../voice/VoiceRecording";
+import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
import SendMessageComposer from "./SendMessageComposer";
+import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
+import { Action } from "../../../dispatcher/actions";
interface IComposerAvatarProps {
me: object;
@@ -316,10 +318,10 @@ export default class MessageComposer extends React.Component {
}
}
- addEmoji(emoji) {
- dis.dispatch({
- action: "insert_emoji",
- emoji,
+ addEmoji(emoji: string) {
+ dis.dispatch({
+ action: Action.ComposerInsert,
+ text: emoji,
});
}
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 3bf9a9db33..cae86846d9 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -14,30 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useContext} from "react";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import React, { useContext } from "react";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { User } from "matrix-js-sdk/src/models/user";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import DMRoomMap from "../../../utils/DMRoomMap";
-import {_t} from "../../../languageHandler";
+import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
-import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader";
+import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader";
import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
-import {Action} from "../../../dispatcher/actions";
+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";
-
+import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
-
import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
-function hasExpectedEncryptionSettings(room): boolean {
- const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
+function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
+ const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
const isPublic: boolean = room.getJoinRule() === "public";
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
}
@@ -61,7 +62,7 @@ const NewRoomIntro = () => {
defaultDispatcher.dispatch({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
- member: member || {userId: dmPartner},
+ member: member || { userId: dmPartner } as User,
});
}} />
@@ -194,7 +195,7 @@ const NewRoomIntro = () => {
return
- { !hasExpectedEncryptionSettings(room) && (
+ { !hasExpectedEncryptionSettings(cli, room) && (
{
}
private renderCommunityInvites(): ReactComponentElement[] {
+ if (SettingsStore.getValue("feature_spaces")) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index ba8bbffbcc..61166b4230 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -45,7 +45,7 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { Enable, Resizable } from "re-resizable";
import { Direction } from "re-resizable/lib/resizer";
import { polyfillTouchEvent } from "../../../@types/polyfill";
-import { ResizeNotifier } from "../../../utils/ResizeNotifier";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index aae182eca4..310ff29010 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -119,7 +119,7 @@ export default class RoomTile extends React.PureComponent {
};
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
- if (!room?.roomId === this.props.room.roomId) return;
+ if (room?.roomId !== this.props.room.roomId) return;
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
};
@@ -316,7 +316,7 @@ export default class RoomTile extends React.PureComponent {
0,
));
} else {
- console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
+ console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`);
}
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js
index a2d4f92d35..66e76903eb 100644
--- a/src/components/views/rooms/RoomUpgradeWarningBar.js
+++ b/src/components/views/rooms/RoomUpgradeWarningBar.js
@@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
-export default class RoomUpgradeWarningBar extends React.Component {
+export default class RoomUpgradeWarningBar extends React.PureComponent {
static propTypes = {
room: PropTypes.object.isRequired,
recommendation: PropTypes.object.isRequired,
diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js
deleted file mode 100644
index 029516c932..0000000000
--- a/src/components/views/rooms/SearchBar.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-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 AccessibleButton from "../elements/AccessibleButton";
-import classNames from "classnames";
-import { _t } from '../../../languageHandler';
-import {Key} from "../../../Keyboard";
-import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-@replaceableComponent("views.rooms.SearchBar")
-export default class SearchBar extends React.Component {
- constructor(props) {
- super(props);
-
- this._search_term = createRef();
-
- this.state = {
- scope: 'Room',
- };
- }
-
- onThisRoomClick = () => {
- this.setState({ scope: 'Room' }, () => this._searchIfQuery());
- };
-
- onAllRoomsClick = () => {
- this.setState({ scope: 'All' }, () => this._searchIfQuery());
- };
-
- onSearchChange = (e) => {
- switch (e.key) {
- case Key.ENTER:
- this.onSearch();
- break;
- case Key.ESCAPE:
- this.props.onCancelClick();
- break;
- }
- };
-
- _searchIfQuery() {
- if (this._search_term.current.value) {
- this.onSearch();
- }
- }
-
- onSearch = () => {
- this.props.onSearch(this._search_term.current.value, this.state.scope);
- };
-
- render() {
- const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
- mx_SearchBar_searching: this.props.searchInProgress,
- });
- const thisRoomClasses = classNames("mx_SearchBar_button", {
- mx_SearchBar_unselected: this.state.scope !== 'Room',
- });
- const allRoomsClasses = classNames("mx_SearchBar_button", {
- mx_SearchBar_unselected: this.state.scope !== 'All',
- });
-
- return (
- <>
-
-
-
- {_t("This Room")}
-
-
- {_t("All Rooms")}
-
-
-
-
-
-
- >
- );
- }
-}
diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx
new file mode 100644
index 0000000000..d71bb8da73
--- /dev/null
+++ b/src/components/views/rooms/SearchBar.tsx
@@ -0,0 +1,130 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+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, RefObject } from 'react';
+import AccessibleButton from "../elements/AccessibleButton";
+import classNames from "classnames";
+import { _t } from '../../../languageHandler';
+import { Key } from "../../../Keyboard";
+import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ onCancelClick: () => void;
+ onSearch: (query: string, scope: string) => void;
+ searchInProgress?: boolean;
+ isRoomEncrypted?: boolean;
+}
+
+interface IState {
+ scope: SearchScope;
+}
+
+export enum SearchScope {
+ Room = "Room",
+ All = "All",
+}
+
+@replaceableComponent("views.rooms.SearchBar")
+export default class SearchBar extends React.Component {
+ private searchTerm: RefObject = createRef();
+
+ constructor(props: IProps) {
+ super(props);
+ this.state = {
+ scope: SearchScope.Room,
+ };
+ }
+
+ private onThisRoomClick = () => {
+ this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery());
+ };
+
+ private onAllRoomsClick = () => {
+ this.setState({ scope: SearchScope.All }, () => this.searchIfQuery());
+ };
+
+ private onSearchChange = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case Key.ENTER:
+ this.onSearch();
+ break;
+ case Key.ESCAPE:
+ this.props.onCancelClick();
+ break;
+ }
+ };
+
+ private searchIfQuery(): void {
+ if (this.searchTerm.current.value) {
+ this.onSearch();
+ }
+ }
+
+ private onSearch = (): void => {
+ this.props.onSearch(this.searchTerm.current.value, this.state.scope);
+ };
+
+ public render() {
+ const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
+ mx_SearchBar_searching: this.props.searchInProgress,
+ });
+ const thisRoomClasses = classNames("mx_SearchBar_button", {
+ mx_SearchBar_unselected: this.state.scope !== SearchScope.Room,
+ });
+ const allRoomsClasses = classNames("mx_SearchBar_button", {
+ mx_SearchBar_unselected: this.state.scope !== SearchScope.All,
+ });
+
+ return (
+ <>
+
+
+
+ {_t("This Room")}
+
+
+ {_t("All Rooms")}
+
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index d60eba00ec..10ef91c689 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -27,27 +27,26 @@ import {
startsWith,
stripPrefix,
} from '../../../editor/serialize';
-import {CommandPartCreator} from '../../../editor/parts';
+import { CommandPartCreator } from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import ReplyThread from "../elements/ReplyThread";
-import {parseEvent} from '../../../editor/deserialize';
-import {findEditableEvent} from '../../../utils/EventUtils';
+import { findEditableEvent } from '../../../utils/EventUtils';
import SendHistoryManager from "../../../SendHistoryManager";
-import {CommandCategories, getCommand} from '../../../SlashCommands';
+import { CommandCategories, getCommand } from '../../../SlashCommands';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
-import {_t, _td} from '../../../languageHandler';
+import { _t, _td } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
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 { Action } from "../../../dispatcher/actions";
+import { containsEmoji } from "../../../effects/utils";
+import { CHAT_EFFECTS } from '../../../effects';
import CountlyAnalytics from "../../../CountlyAnalytics";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import EMOJI_REGEX from 'emojibase-regex';
-import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from '../../../settings/SettingsStore';
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
@@ -486,62 +485,18 @@ export default class SendMessageComposer extends React.Component {
case Action.FocusComposer:
this._editorRef && this._editorRef.focus();
break;
- case 'insert_mention':
- this._insertMention(payload.user_id);
- break;
- case 'quote':
- this._insertQuotedMessage(payload.event);
- break;
- case 'insert_emoji':
- this._insertEmoji(payload.emoji);
+ case "send_composer_insert":
+ if (payload.userId) {
+ this._editorRef && this._editorRef.insertMention(payload.userId);
+ } else if (payload.event) {
+ this._editorRef && this._editorRef.insertQuotedMessage(payload.event);
+ } else if (payload.text) {
+ this._editorRef && this._editorRef.insertPlaintext(payload.text);
+ }
break;
}
};
- _insertMention(userId) {
- const {model} = this;
- const {partCreator} = model;
- const member = this.props.room.getMember(userId);
- const displayName = member ?
- member.rawDisplayName : userId;
- const caret = this._editorRef.getCaret();
- const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
- // Insert suffix only if the caret is at the start of the composer
- const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId);
- model.transform(() => {
- const addedLen = model.insert(parts, position);
- return model.positionForOffset(caret.offset + addedLen, true);
- });
- // refocus on composer, as we just clicked "Mention"
- this._editorRef && this._editorRef.focus();
- }
-
- _insertQuotedMessage(event) {
- const {model} = this;
- const {partCreator} = model;
- const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
- // add two newlines
- quoteParts.push(partCreator.newline());
- quoteParts.push(partCreator.newline());
- model.transform(() => {
- const addedLen = model.insert(quoteParts, model.positionForOffset(0));
- return model.positionForOffset(addedLen, true);
- });
- // refocus on composer, as we just clicked "Quote"
- this._editorRef && this._editorRef.focus();
- }
-
- _insertEmoji = (emoji) => {
- const {model} = this;
- const {partCreator} = model;
- const caret = this._editorRef.getCaret();
- const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
- model.transform(() => {
- const addedLen = model.insert([partCreator.plain(emoji)], position);
- return model.positionForOffset(caret.offset + addedLen, true);
- });
- };
-
_onPaste = (event) => {
const {clipboardData} = event;
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index 2102071bf3..122ba0ca0b 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback";
import {MsgType} from "matrix-js-sdk/src/@types/event";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
-import CallMediaHandler from "../../../CallMediaHandler";
+import MediaDeviceHandler from "../../../MediaDeviceHandler";
interface IProps {
room: Room;
@@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)),
},
- "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint
+ "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
await this.disposeRecording();
}
@@ -132,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent
diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx
index 3a1d2051b4..93078ff645 100644
--- a/src/components/views/rooms/WhoIsTypingTile.tsx
+++ b/src/components/views/rooms/WhoIsTypingTile.tsx
@@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
-import Room from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
similarity index 61%
rename from src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index 28aad65129..c4963d0154 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -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.
@@ -15,68 +15,76 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from "../../../../../languageHandler";
-import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
-import * as sdk from "../../../../..";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+
+import { _t } from "../../../../../languageHandler";
+import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
+import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
+import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
-import {replaceableComponent} from "../../../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ closeSettingsFn(): void;
+}
+
+interface IRecommendedVersion {
+ version: string;
+ needsUpgrade: boolean;
+ urgent: boolean;
+}
+
+interface IState {
+ upgradeRecommendation?: IRecommendedVersion;
+ oldRoomId?: string;
+ oldEventId?: string;
+ upgraded?: boolean;
+}
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
-export default class AdvancedRoomSettingsTab extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- closeSettingsFn: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
+export default class AdvancedRoomSettingsTab extends React.Component {
+ constructor(props, context) {
+ super(props, context);
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
};
- }
- // TODO: [REACT-WARNING] Move this to constructor
- UNSAFE_componentWillMount() { // eslint-disable-line camelcase
// we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room.getRecommendedVersion().then((v) => {
- const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
+ const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
- const additionalStateChanges = {};
- const createEvent = room.currentState.getStateEvents("m.room.create", "");
+ const additionalStateChanges: Partial = {};
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
if (predecessor && predecessor.room_id) {
- additionalStateChanges['oldRoomId'] = predecessor.room_id;
- additionalStateChanges['oldEventId'] = predecessor.event_id;
- additionalStateChanges['hasPreviousRoom'] = true;
+ additionalStateChanges.oldRoomId = predecessor.room_id;
+ additionalStateChanges.oldEventId = predecessor.event_id;
}
-
this.setState({
- upgraded: tombstone && tombstone.getContent().replacement_room,
+ upgraded: !!tombstone?.getContent().replacement_room,
upgradeRecommendation: v,
...additionalStateChanges,
});
});
}
- _upgradeRoom = (e) => {
- const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
+ private upgradeRoom = (e) => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
- Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
+ Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
};
- _openDevtools = (e) => {
- const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
+ private openDevtools = (e) => {
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
};
- _onOldRoomClicked = (e) => {
+ private onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
@@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
const room = client.getRoom(this.props.roomId);
let unfederatableSection;
- const createEvent = room.currentState.getStateEvents('m.room.create', '');
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
- unfederatableSection = {_t('This room is not accessible by remote Matrix servers')}
;
+ unfederatableSection = { _t('This room is not accessible by remote Matrix servers') }
;
}
let roomUpgradeButton;
@@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
roomUpgradeButton = (
- {_t(
+ { _t(
"Warning : Upgrading a room will not automatically migrate room members " +
"to the new version of the room. We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
@@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
"b": (sub) => {sub} ,
"i": (sub) => {sub} ,
},
- )}
+ ) }
-
- {_t("Upgrade this room to the recommended room version")}
+
+ { _t("Upgrade this room to the recommended room version") }
);
}
let oldRoomLink;
- if (this.state.hasPreviousRoom) {
+ if (this.state.oldRoomId) {
let name = _t("this room");
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room && room.name) name = room.name;
oldRoomLink = (
-
- {_t("View older messages in %(roomName)s.", {roomName: name})}
+
+ { _t("View older messages in %(roomName)s.", { roomName: name }) }
);
}
return (
-
{_t("Advanced")}
+
{ _t("Advanced") }
-
{_t("Room information")}
+
+ { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
+
- {_t("Internal room ID:")}
- {this.props.roomId}
+ { _t("Internal room ID:") }
+ { this.props.roomId }
- {unfederatableSection}
+ { unfederatableSection }
-
{_t("Room version")}
+
{ _t("Room version") }
- {_t("Room version:")}
- {room.getVersion()}
+ { _t("Room version:") }
+ { room.getVersion() }
- {oldRoomLink}
- {roomUpgradeButton}
+ { oldRoomLink }
+ { roomUpgradeButton }
-
{_t("Developer options")}
-
- {_t("Open Devtools")}
+ { _t("Developer options") }
+
+ { _t("Open Devtools") }
diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
index 8d886a191e..25dfe2f151 100644
--- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx
@@ -44,14 +44,11 @@ export default class BridgeSettingsTab extends React.Component {
return ;
}
- static getBridgeStateEvents(roomId: string) {
+ static getBridgeStateEvents(roomId: string): MatrixEvent[] {
const client = MatrixClientPeg.get();
const roomState = client.getRoom(roomId).currentState;
- return BRIDGE_EVENT_TYPES.map(typeName => {
- const events = roomState.events.get(typeName);
- return events ? Array.from(events.values()) : [];
- }).flat(1);
+ return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1);
}
render() {
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 139cfd5fbd..10c93c5dca 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
- const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
@@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
+ canonicalAliasEvent={canonicalAliasEv} />
{_t("Other")}
{ flairSection }
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 02bbcfb751..bb7e194253 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
// Knock and private are reserved keywords which are not yet implemented.
-enum JoinRule {
+export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
Private = "private",
}
-enum GuestAccess {
+export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
-enum HistoryVisibility {
+export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
+ private onEncryptionChange = () => {
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'),
description: _t(
diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
index 3fa0be478c..beff033001 100644
--- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx
@@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu";
import { toRightOf } from "../../../../structures/ContextMenu";
interface IProps {
- closeSettingsFn: () => {};
+ closeSettingsFn: () => void;
}
interface IState {
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index 362059f8ed..f730406eed 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import {_t} from "../../../../../languageHandler";
import SdkConfig from "../../../../../SdkConfig";
-import CallMediaHandler from "../../../../../CallMediaHandler";
+import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
import Field from "../../../elements/Field";
import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
@@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
}
async componentDidMount() {
- const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
+ const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
if (canSeeDeviceLabels) {
this._refreshMediaDevices();
}
@@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
_refreshMediaDevices = async (stream) => {
this.setState({
- mediaDevices: await CallMediaHandler.getDevices(),
- activeAudioOutput: CallMediaHandler.getAudioOutput(),
- activeAudioInput: CallMediaHandler.getAudioInput(),
- activeVideoInput: CallMediaHandler.getVideoInput(),
+ mediaDevices: await MediaDeviceHandler.getDevices(),
+ activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
+ activeAudioInput: MediaDeviceHandler.getAudioInput(),
+ activeVideoInput: MediaDeviceHandler.getVideoInput(),
});
if (stream) {
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
@@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
};
_setAudioOutput = (e) => {
- CallMediaHandler.setAudioOutput(e.target.value);
+ MediaDeviceHandler.instance.setAudioOutput(e.target.value);
this.setState({
activeAudioOutput: e.target.value,
});
};
_setAudioInput = (e) => {
- CallMediaHandler.setAudioInput(e.target.value);
+ MediaDeviceHandler.instance.setAudioInput(e.target.value);
this.setState({
activeAudioInput: e.target.value,
});
};
_setVideoInput = (e) => {
- CallMediaHandler.setVideoInput(e.target.value);
+ MediaDeviceHandler.instance.setVideoInput(e.target.value);
this.setState({
activeVideoInput: e.target.value,
});
@@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
}
};
- const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
+ const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
if (audioOutputs.length > 0) {
const defaultDevice = getDefaultDevice(audioOutputs);
speakerDropdown = (
@@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
);
}
- const audioInputs = this.state.mediaDevices.audioinput.slice(0);
+ const audioInputs = this.state.mediaDevices.audioInput.slice(0);
if (audioInputs.length > 0) {
const defaultDevice = getDefaultDevice(audioInputs);
microphoneDropdown = (
@@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
);
}
- const videoInputs = this.state.mediaDevices.videoinput.slice(0);
+ const videoInputs = this.state.mediaDevices.videoInput.slice(0);
if (videoInputs.length > 0) {
const defaultDevice = getDefaultDevice(videoInputs);
webcamDropdown = (
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 6a935ab276..2d096e1b9f 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -29,12 +29,13 @@ import AccessibleButton from "../elements/AccessibleButton";
import {BetaPill} from "../beta/BetaCard";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
-import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
+import { UserTab } from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
+import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
],
});
+const nameToAlias = (name: string, domain: string): string => {
+ const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
+ return `#${localpart}:${domain}`;
+};
+
const SpaceCreateMenu = ({ onFinished }) => {
const cli = useContext(MatrixClientContext);
const [visibility, setVisibility] = useState(null);
@@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
const [name, setName] = useState("");
const spaceNameField = useRef();
+ const [alias, setAlias] = useState("");
+ const spaceAliasField = useRef();
const [avatar, setAvatar] = useState(null);
const [topic, setTopic] = useState("");
@@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
setBusy(false);
return;
}
+ // validate the space name alias field but do not require it
+ if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
+ spaceAliasField.current.focus();
+ spaceAliasField.current.validate({ allowEmpty: true, focused: true });
+ setBusy(false);
+ return;
+ }
const initialState: ICreateRoomStateEvent[] = [
{
@@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
content: { url },
});
}
- if (topic) {
- initialState.push({
- type: EventType.RoomTopic,
- content: { topic },
- });
- }
try {
await createRoom({
@@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
creation_content: {
- // Based on MSC1840
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: initialState,
@@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
},
+ room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
+ topic,
},
spinner: false,
encryption: false,
@@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
;
} else {
+ const domain = cli.getDomain();
body =
{
label={_t("Name")}
autoFocus={true}
value={name}
- onChange={ev => setName(ev.target.value)}
+ onChange={ev => {
+ const newName = ev.target.value;
+ if (!alias || alias === nameToAlias(name, domain)) {
+ setAlias(nameToAlias(newName, domain));
+ }
+ setName(newName);
+ }}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/>
+ { visibility === Visibility.Public
+ ?
+ : null
+ }
+
{
onFinished();
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
- initialTabId: USER_LABS_TAB,
+ initialTabId: UserTab.Labs,
});
}} />
{ body }
diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index eb63b21f0e..b33fcf8915 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -14,18 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useEffect, useState } from "react";
+import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
+import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import classNames from "classnames";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
-import {_t} from "../../../languageHandler";
+import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
-import {useContextMenu} from "../../structures/ContextMenu";
+import { useContextMenu } from "../../structures/ContextMenu";
import SpaceCreateMenu from "./SpaceCreateMenu";
-import {SpaceItem} from "./SpaceTreeLevel";
+import { SpaceItem } from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
import SpaceStore, {
+ HOME_SPACE,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
@@ -37,9 +39,10 @@ import {
RovingAccessibleTooltipButton,
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
-import {Key} from "../../../Keyboard";
-import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
-import {NotificationState} from "../../../stores/notifications/NotificationState";
+import { Key } from "../../../Keyboard";
+import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
+import { NotificationState } from "../../../stores/notifications/NotificationState";
+import SettingsStore from "../../../settings/SettingsStore";
interface IButtonProps {
space?: Room;
@@ -120,11 +123,65 @@ const useSpaces = (): [Room[], Room[], Room | null] => {
return [invites, spaces, activeSpace];
};
+interface IInnerSpacePanelProps {
+ children?: ReactNode;
+ isPanelCollapsed: boolean;
+ setPanelCollapsed: Dispatch>;
+}
+
+// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
+const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => {
+ const [invites, spaces, activeSpace] = useSpaces();
+ const activeSpaces = activeSpace ? [activeSpace] : [];
+
+ const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
+ ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
+
+ return
+ SpaceStore.instance.setActiveSpace(null)}
+ selected={!activeSpace}
+ tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
+ notificationState={homeNotificationState}
+ isNarrow={isPanelCollapsed}
+ />
+ { invites.map(s => (
+ setPanelCollapsed(false)}
+ />
+ )) }
+ { spaces.map((s, i) => (
+
+ {(provided, snapshot) => (
+ setPanelCollapsed(false)}
+ />
+ )}
+
+ )) }
+ { children }
+
;
+});
+
const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
- const [invites, spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
useEffect(() => {
@@ -133,10 +190,6 @@ const SpacePanel = () => {
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
- const newClasses = classNames("mx_SpaceButton_new", {
- mx_SpaceButton_newCancel: menuDisplayed,
- });
-
let contextMenu = null;
if (menuDisplayed) {
contextMenu = ;
@@ -203,59 +256,61 @@ const SpacePanel = () => {
}
};
- const activeSpaces = activeSpace ? [activeSpace] : [];
- const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
- // TODO drag and drop for re-arranging order
- return
- {({onKeyDownHandler}) => (
-
-
-
-
SpaceStore.instance.setActiveSpace(null)}
- selected={!activeSpace}
- tooltip={_t("All rooms")}
- notificationState={RoomNotificationStateStore.instance.globalState}
- isNarrow={isPanelCollapsed}
+ const onNewClick = menuDisplayed ? closeMenu : () => {
+ if (!isPanelCollapsed) setPanelCollapsed(true);
+ openMenu();
+ };
+
+ return (
+ {
+ if (!result.destination) return; // dropped outside the list
+ SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
+ }}>
+
+ {({onKeyDownHandler}) => (
+
+
+ {(provided, snapshot) => (
+
+
+ { provided.placeholder }
+
+
+
+
+ )}
+
+ setPanelCollapsed(!isPanelCollapsed)}
+ title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
- { invites.map(s => setPanelCollapsed(false)}
- />) }
- { spaces.map(s => setPanelCollapsed(false)}
- />) }
-
- {
- if (!isPanelCollapsed) setPanelCollapsed(true);
- openMenu();
- }}
- isNarrow={isPanelCollapsed}
- />
-
- setPanelCollapsed(!isPanelCollapsed)}
- title={expandCollapseButtonTitle}
- />
- { contextMenu }
-
- )}
-
+ { contextMenu }
+
+ )}
+
+
+ );
};
export default SpacePanel;
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
new file mode 100644
index 0000000000..3afdc629e4
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -0,0 +1,143 @@
+/*
+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, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
+import SpaceBasicSettings from "./SpaceBasicSettings";
+import { avatarUrlForRoom } from "../../../Avatar";
+import { IDialogProps } from "../dialogs/IDialogProps";
+import { getTopic } from "../elements/RoomTopic";
+import { defaultDispatcher } from "../../../dispatcher/dispatcher";
+
+interface IProps extends IDialogProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
+ const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
+ const avatarChanged = newAvatar !== null;
+
+ const [name, setName] = useState(space.name);
+ const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
+ const nameChanged = name !== space.name;
+
+ const currentTopic = getTopic(space);
+ const [topic, setTopic] = useState(currentTopic);
+ const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
+ const topicChanged = topic !== currentTopic;
+
+ const onCancel = () => {
+ setNewAvatar(null);
+ setName(space.name);
+ setTopic(currentTopic);
+ };
+
+ const onSave = async () => {
+ setBusy(true);
+ const promises = [];
+
+ if (avatarChanged) {
+ if (newAvatar) {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
+ url: await cli.uploadContent(newAvatar),
+ }, ""));
+ } else {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
+ }
+ }
+
+ if (nameChanged) {
+ promises.push(cli.setRoomName(space.roomId, name));
+ }
+
+ if (topicChanged) {
+ promises.push(cli.setRoomTopic(space.roomId, topic));
+ }
+
+ const results = await Promise.allSettled(promises);
+ setBusy(false);
+ const failures = results.filter(r => r.status === "rejected");
+ if (failures.length > 0) {
+ console.error("Failed to save space settings: ", failures);
+ setError(_t("Failed to save space settings."));
+ }
+ };
+
+ return
+
{ _t("General") }
+
+
{ _t("Edit settings relating to your space.") }
+
+ { error &&
{ error }
}
+
+
onFinished(false)} />
+
+
+
+
+
+ { _t("Cancel") }
+
+
+ { busy ? _t("Saving...") : _t("Save Changes") }
+
+
+
+ {_t("Leave Space")}
+
+
{
+ defaultDispatcher.dispatch({
+ action: "leave_room",
+ room_id: space.roomId,
+ });
+ }}
+ >
+ { _t("Leave Space") }
+
+
+ ;
+};
+
+export default SpaceSettingsGeneralTab;
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
new file mode 100644
index 0000000000..263823603b
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -0,0 +1,187 @@
+/*
+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, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import AliasSettings from "../room_settings/AliasSettings";
+import { useStateToggle } from "../../../hooks/useStateToggle";
+import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
+import StyledRadioGroup from "../elements/StyledRadioGroup";
+
+interface IProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+enum SpaceVisibility {
+ Unlisted = "unlisted",
+ Private = "private",
+}
+
+const useLocalEcho = (
+ currentFactory: () => T,
+ setterFn: (value: T) => Promise,
+ errorFn: (error: Error) => void,
+): [value: T, handler: (value: T) => void] => {
+ const [value, setValue] = useState(currentFactory);
+ const handler = async (value: T) => {
+ setValue(value);
+ try {
+ await setterFn(value);
+ } catch (e) {
+ setValue(currentFactory());
+ errorFn(e);
+ }
+ };
+
+ return [value, handler];
+};
+
+const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [visibility, setVisibility] = useLocalEcho(
+ () => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
+ visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
+ join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
+ }, ""),
+ () => setError(_t("Failed to update the visibility of this space")),
+ );
+ const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
+ ?.getContent()?.guest_access === GuestAccess.CanJoin,
+ guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
+ guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
+ }, ""),
+ () => setError(_t("Failed to update the guest access of this space")),
+ );
+ const [historyVisibility, setHistoryVisibility] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
+ ?.getContent()?.history_visibility || HistoryVisibility.Shared,
+ historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
+ history_visibility: historyVisibility,
+ }, ""),
+ () => setError(_t("Failed to update the history visibility of this space")),
+ );
+
+ const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
+
+ const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
+ const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
+ const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
+ const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
+ const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
+
+ let advancedSection;
+ if (showAdvancedSection) {
+ advancedSection = <>
+
+ { _t("Hide advanced") }
+
+
+
+
+ { _t("Guests can join a space without having an account.") }
+
+ { _t("This may be useful for public spaces.") }
+
+ >;
+ } else {
+ advancedSection = <>
+
+ { _t("Show advanced") }
+
+ >;
+ }
+
+ let addressesSection;
+ if (visibility !== SpaceVisibility.Private) {
+ addressesSection = <>
+ {_t("Address")}
+
+ >;
+ }
+
+ return
+
{ _t("Visibility") }
+
+ { error &&
{ error }
}
+
+
+
+ { _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
+
+
+
+
+
+
+ { advancedSection }
+
+
{
+ setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
+ }}
+ disabled={!canSetHistoryVisibility}
+ label={_t("Preview Space")}
+ />
+ { _t("Allow people to preview your space before they join.") }
+ { _t("Recommended for public spaces.") }
+
+
+ { addressesSection }
+
;
+};
+
+export default SpaceSettingsVisibilityTab;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index f34baf256b..75ca641320 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
import classNames from "classnames";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
-import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
-import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
+import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
-import {_t} from "../../../languageHandler";
-import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
-import {toRightOf} from "../../structures/ContextMenu";
+import { _t } from "../../../languageHandler";
+import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
+import { toRightOf } from "../../structures/ContextMenu";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
@@ -39,33 +38,38 @@ import {
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {Action} from "../../../dispatcher/actions";
+import { Action } from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
-import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {EventType} from "matrix-js-sdk/src/@types/event";
-import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
-import {NotificationColor} from "../../../stores/notifications/NotificationColor";
+import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
+import { NotificationColor } from "../../../stores/notifications/NotificationColor";
+import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
-interface IItemProps {
+interface IItemProps extends InputHTMLAttributes {
space?: Room;
activeSpaces: Room[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
parents?: Set;
+ innerRef?: LegacyRef;
}
interface IItemState {
collapsed: boolean;
contextMenuPosition: Pick;
+ childSpaces: Room[];
}
export class SpaceItem extends React.PureComponent {
static contextType = MatrixClientContext;
+ private buttonRef = createRef();
+
constructor(props) {
super(props);
@@ -78,14 +82,36 @@ export class SpaceItem extends React.PureComponent {
this.state = {
collapsed: collapsed,
contextMenuPosition: null,
+ childSpaces: this.childSpaces,
};
+
+ SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
}
- private toggleCollapse(evt) {
- if (this.props.onExpand && this.state.collapsed) {
+ componentWillUnmount() {
+ SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
+ }
+
+ private onSpaceUpdate = () => {
+ this.setState({
+ childSpaces: this.childSpaces,
+ });
+ };
+
+ private get childSpaces() {
+ return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
+ .filter(s => !this.props.parents?.has(s.roomId));
+ }
+
+ private get isCollapsed() {
+ return this.state.collapsed || this.props.isPanelCollapsed;
+ }
+
+ private toggleCollapse = evt => {
+ if (this.props.onExpand && this.isCollapsed) {
this.props.onExpand();
}
- const newCollapsedState = !this.state.collapsed;
+ const newCollapsedState = !this.isCollapsed;
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
this.props.space.roomId,
@@ -96,7 +122,7 @@ export class SpaceItem extends React.PureComponent {
// don't bubble up so encapsulating button for space
// doesn't get triggered
evt.stopPropagation();
- }
+ };
private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return;
@@ -111,6 +137,43 @@ export class SpaceItem extends React.PureComponent {
});
}
+ private onKeyDown = (ev: React.KeyboardEvent) => {
+ let handled = true;
+ const action = getKeyBindingsManager().getRoomListAction(ev);
+ const hasChildren = this.state.childSpaces?.length;
+ switch (action) {
+ case RoomListAction.CollapseSection:
+ if (hasChildren && !this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
+ const parentButton = parentItem?.previousElementSibling as HTMLElement;
+ parentButton?.focus();
+ }
+ break;
+
+ case RoomListAction.ExpandSection:
+ if (hasChildren) {
+ if (this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const childLevel = this.buttonRef?.current?.nextElementSibling;
+ const firstSpaceItemChild = childLevel?.querySelector(".mx_SpaceItem");
+ firstSpaceItemChild?.querySelector(".mx_SpaceButton")?.focus();
+ }
+ }
+ break;
+
+ default:
+ handled = false;
+ }
+
+ if (handled) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ };
+
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -300,27 +363,25 @@ export class SpaceItem extends React.PureComponent {
}
render() {
- const {space, activeSpaces, isNested} = this.props;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
+ ...otherProps } = this.props;
- const forceCollapsed = this.props.isPanelCollapsed;
- const isNarrow = this.props.isPanelCollapsed;
- const collapsed = this.state.collapsed || forceCollapsed;
+ const collapsed = this.isCollapsed;
- const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
- .filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space);
- const itemClasses = classNames({
+ const itemClasses = classNames(this.props.className, {
"mx_SpaceItem": true,
- "mx_SpaceItem_narrow": isNarrow,
+ "mx_SpaceItem_narrow": isPanelCollapsed,
"collapsed": collapsed,
- "hasSubSpaces": childSpaces && childSpaces.length,
+ "hasSubSpaces": this.state.childSpaces?.length,
});
const isInvite = space.getMyMembership() === "invite";
const classes = classNames("mx_SpaceButton", {
mx_SpaceButton_active: isActive,
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
- mx_SpaceButton_narrow: isNarrow,
+ mx_SpaceButton_narrow: isPanelCollapsed,
mx_SpaceButton_invite: isInvite,
});
const notificationState = isInvite
@@ -328,12 +389,12 @@ export class SpaceItem extends React.PureComponent {
: SpaceStore.instance.getNotificationState(space.roomId);
let childItems;
- if (childSpaces && !collapsed) {
+ if (this.state.childSpaces?.length && !collapsed) {
childItems = ;
}
@@ -346,53 +407,36 @@ export class SpaceItem extends React.PureComponent {
const avatarSize = isNested ? 24 : 32;
- const toggleCollapseButton = childSpaces && childSpaces.length ?
+ const toggleCollapseButton = this.state.childSpaces?.length ?
this.toggleCollapse(evt)}
+ onClick={this.toggleCollapse}
+ tabIndex={-1}
+ aria-label={collapsed ? _t("Expand") : _t("Collapse")}
/> : null;
- let button;
- if (isNarrow) {
- button = (
+ return (
+
{ toggleCollapseButton }
+ { !isPanelCollapsed && { space.name } }
{ notifBadge }
{ this.renderContextMenu() }
- );
- } else {
- button = (
-
- { toggleCollapseButton }
-
-
- { space.name }
- { notifBadge }
- { this.renderContextMenu() }
-
-
- );
- }
- return (
-
- { button }
{ childItems }
);
diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx
index 209babbf9d..45b65ae1fb 100644
--- a/src/components/views/toasts/GenericToast.tsx
+++ b/src/components/views/toasts/GenericToast.tsx
@@ -15,8 +15,8 @@ limitations under the License.
*/
import React, {ReactNode} from "react";
+import AccessibleButton from "../elements/AccessibleButton";
-import FormButton from "../elements/FormButton";
import {XOR} from "../../../@types/common";
export interface IProps {
@@ -50,8 +50,12 @@ const GenericToast: React.FC> = ({
{detailContent}
- {onReject && rejectLabel &&
}
-
+ {onReject && rejectLabel &&
+ { rejectLabel }
+ }
+
+ { acceptLabel }
+
;
};
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index c78f0c0fc8..d29caf789e 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React, {createRef} from 'react';
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
import { logger } from 'matrix-js-sdk/src/logger';
-import CallMediaHandler from "../../../CallMediaHandler";
+import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
interface IProps {
feed: CallFeed,
@@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component {
private element = createRef();
componentDidMount() {
+ MediaDeviceHandler.instance.addListener(
+ MediaDeviceHandlerEvent.AudioOutputChanged,
+ this.onAudioOutputChanged,
+ );
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
this.playMedia();
}
componentWillUnmount() {
+ MediaDeviceHandler.instance.removeListener(
+ MediaDeviceHandlerEvent.AudioOutputChanged,
+ this.onAudioOutputChanged,
+ );
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
this.stopMedia();
}
- private playMedia() {
+ private onAudioOutputChanged = (audioOutput: string) => {
const element = this.element.current;
- const audioOutput = CallMediaHandler.getAudioOutput();
-
if (audioOutput) {
try {
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
@@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component {
logger.warn("Couldn't set requested audio output device: using default", e);
}
}
+ }
+ private playMedia() {
+ const element = this.element.current;
+ this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
element.muted = false;
element.srcObject = this.props.feed.stream;
element.autoplay = true;
diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx
index 2abdc0641d..c09043da24 100644
--- a/src/components/views/voip/IncomingCallBox.tsx
+++ b/src/components/views/voip/IncomingCallBox.tsx
@@ -21,17 +21,20 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads';
-import CallHandler from '../../../CallHandler';
+import CallHandler, { AudioID } from '../../../CallHandler';
import RoomAvatar from '../avatars/RoomAvatar';
-import FormButton from '../elements/FormButton';
+import AccessibleButton from '../elements/AccessibleButton';
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent";
+import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
+import classNames from 'classnames';
interface IProps {
}
interface IState {
incomingCall: any;
+ silenced: boolean;
}
@replaceableComponent("views.voip.IncomingCallBox")
@@ -44,6 +47,7 @@ export default class IncomingCallBox extends React.Component {
this.dispatcherRef = dis.register(this.onAction);
this.state = {
incomingCall: null,
+ silenced: false,
};
}
@@ -58,6 +62,7 @@ export default class IncomingCallBox extends React.Component {
if (call && call.state === CallState.Ringing) {
this.setState({
incomingCall: call,
+ silenced: false, // Reset silenced state for new call
});
} else {
this.setState({
@@ -84,6 +89,13 @@ export default class IncomingCallBox extends React.Component {
});
};
+ private onSilenceClick: React.MouseEventHandler = (e) => {
+ e.stopPropagation();
+ const newState = !this.state.silenced
+ this.setState({silenced: newState});
+ newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring);
+ }
+
public render() {
if (!this.state.incomingCall) {
return null;
@@ -107,6 +119,12 @@ export default class IncomingCallBox extends React.Component {
}
}
+ const silenceClass = classNames({
+ "mx_IncomingCallBox_iconButton": true,
+ "mx_IncomingCallBox_unSilence": this.state.silenced,
+ "mx_IncomingCallBox_silence": !this.state.silenced,
+ });
+
return
{
{caller}
{incomingCallText}
+
-
+ >
+ { _t("Decline") }
+
-
+ >
+ { _t("Accept") }
+
;
}
}
-
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index f9d957b60c..37e91fc54b 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-import {MatrixClientPeg} from "../MatrixClientPeg";
-import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
-import {ResizeMethod} from "../Avatar";
-import {MatrixClient} from "matrix-js-sdk/src/client";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
+
+import { MatrixClientPeg } from "../MatrixClientPeg";
+import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
// Populate this class with the details of your customisations when copying it.
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 300eed2b98..6438ecc0f2 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -159,4 +159,14 @@ export enum Action {
* Fired when joining a room failed
*/
JoinRoomError = "join_room_error",
+
+ /**
+ * Inserts content into the active composer. Should be used with ComposerInsertPayload
+ */
+ ComposerInsert = "composer_insert",
+
+ /**
+ * Switches space. Should be used with SwitchSpacePayload.
+ */
+ SwitchSpace = "switch_space",
}
diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts
new file mode 100644
index 0000000000..9702855432
--- /dev/null
+++ b/src/dispatcher/payloads/ComposerInsertPayload.ts
@@ -0,0 +1,42 @@
+/*
+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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
+import { ActionPayload } from "../payloads";
+import { Action } from "../actions";
+
+interface IBaseComposerInsertPayload extends ActionPayload {
+ action: Action.ComposerInsert,
+}
+
+interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
+ userId: string;
+}
+
+interface IComposerInsertQuotePayload extends IBaseComposerInsertPayload {
+ event: MatrixEvent;
+}
+
+interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload {
+ text: string;
+}
+
+export type ComposerInsertPayload =
+ IComposerInsertMentionPayload |
+ IComposerInsertQuotePayload |
+ IComposerInsertPlaintextPayload;
+
diff --git a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
index 430fad6145..a4dcb8cfe1 100644
--- a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
+++ b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts
@@ -17,6 +17,7 @@ limitations under the License.
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
@@ -29,7 +30,7 @@ export interface SetRightPanelPhasePayload extends ActionPayload {
}
export interface SetRightPanelPhaseRefireParams {
- member?: RoomMember;
+ member?: RoomMember | User;
verificationRequest?: VerificationRequest;
groupId?: string;
groupRoomId?: string;
diff --git a/src/dispatcher/payloads/SwitchSpacePayload.ts b/src/dispatcher/payloads/SwitchSpacePayload.ts
new file mode 100644
index 0000000000..04eb744334
--- /dev/null
+++ b/src/dispatcher/payloads/SwitchSpacePayload.ts
@@ -0,0 +1,27 @@
+/*
+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 { ActionPayload } from "../payloads";
+import { Action } from "../actions";
+
+export interface SwitchSpacePayload extends ActionPayload {
+ action: Action.SwitchSpace;
+
+ /**
+ * The number of the space to switch to, 1-indexed, 0 is Home.
+ */
+ num: number;
+}
diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts
index c2838d0dbb..c4d73aea6a 100644
--- a/src/dispatcher/payloads/ViewUserPayload.ts
+++ b/src/dispatcher/payloads/ViewUserPayload.ts
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
@@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload {
* The member to view. May be null or falsy to indicate that no member
* should be shown (hide whichever relevant components).
*/
- member?: RoomMember;
+ member?: RoomMember | User;
}
diff --git a/src/editor/model.ts b/src/editor/model.ts
index f1b6f90957..7925a43164 100644
--- a/src/editor/model.ts
+++ b/src/editor/model.ts
@@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {diffAtCaret, diffDeletion, IDiff} from "./diff";
-import DocumentPosition, {IPosition} from "./position";
+import { diffAtCaret, diffDeletion, IDiff } from "./diff";
+import DocumentPosition, { IPosition } from "./position";
import Range from "./range";
-import {SerializedPart, Part, PartCreator} from "./parts";
-import AutocompleteWrapperModel, {ICallback} from "./autocomplete";
+import { SerializedPart, Part, PartCreator } from "./parts";
+import AutocompleteWrapperModel, { ICallback } from "./autocomplete";
import DocumentOffset from "./offset";
-import {Caret} from "./caret";
+import { Caret } from "./caret";
/**
* @callback ModelCallback
@@ -390,7 +390,7 @@ export default class EditorModel {
return addLen;
}
- positionForOffset(totalOffset: number, atPartEnd: boolean) {
+ positionForOffset(totalOffset: number, atPartEnd = false) {
let currentOffset = 0;
const index = this._parts.findIndex(part => {
const partLen = part.text.length;
diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts
index fe71ed9ecd..0384b3bf77 100644
--- a/src/hooks/useAccountData.ts
+++ b/src/hooks/useAccountData.ts
@@ -21,11 +21,11 @@ import {Room} from "matrix-js-sdk/src/models/room";
import {useEventEmitter} from "./useEventEmitter";
-const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined;
+const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined;
// Hook to simplify listening to Matrix account data
export const useAccountData = (cli: MatrixClient, eventType: string) => {
- const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType)));
+ const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType)));
const handler = useCallback((event) => {
if (event.getType() !== eventType) return;
@@ -38,7 +38,7 @@ export const useAccountData = (cli: MatrixClient, eventType: strin
// Hook to simplify listening to Matrix room account data
export const useRoomAccountData = (room: Room, eventType: string) => {
- const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType)));
+ const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType)));
const handler = useCallback((event) => {
if (event.getType() !== eventType) return;
diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts
new file mode 100644
index 0000000000..e778acf8a9
--- /dev/null
+++ b/src/hooks/useRoomState.ts
@@ -0,0 +1,46 @@
+/*
+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 { useCallback, useEffect, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
+
+import { useEventEmitter } from "./useEventEmitter";
+
+type Mapper = (roomState: RoomState) => T;
+const defaultMapper: Mapper = (roomState: RoomState) => roomState;
+
+// Hook to simplify watching Matrix Room state
+export const useRoomState = (
+ room: Room,
+ mapper: Mapper = defaultMapper as Mapper,
+): T => {
+ const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
+
+ const update = useCallback(() => {
+ if (!room) return;
+ setValue(mapper(room.currentState));
+ }, [room, mapper]);
+
+ useEventEmitter(room?.currentState, "RoomState.events", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setValue(undefined);
+ };
+ }, [update]);
+ return value;
+};
diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts
index b50a923234..33701c4f16 100644
--- a/src/hooks/useStateToggle.ts
+++ b/src/hooks/useStateToggle.ts
@@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react";
// Hook to simplify toggling of a boolean state value
// Returns value, method to toggle boolean value and method to set the boolean value
-export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => {
+export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => {
const [value, setValue] = useState(initialValue);
const toggleValue = () => {
setValue(!value);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 57bf5fa739..429ffbedef 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -784,6 +784,7 @@
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
+ "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Spaces": "Spaces",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
@@ -793,6 +794,10 @@
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
+ "Show all rooms in Home": "Show all rooms in Home",
+ "Show people in spaces": "Show people in spaces",
+ "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
+ "Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
@@ -905,6 +910,8 @@
"Incoming voice call": "Incoming voice call",
"Incoming video call": "Incoming video call",
"Incoming call": "Incoming call",
+ "Sound on": "Sound on",
+ "Silence call": "Silence call",
"Decline": "Decline",
"Accept": "Accept",
"Pause": "Pause",
@@ -1016,17 +1023,42 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
+ "e.g. my-space": "e.g. my-space",
+ "Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
+ "All rooms": "All rooms",
+ "Home": "Home",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
- "All rooms": "All rooms",
"Click to copy": "Click to copy",
"Copied!": "Copied!",
"Failed to copy": "Failed to copy",
"Share invite link": "Share invite link",
"Invite people": "Invite people",
"Invite with email or username": "Invite with email or username",
+ "Failed to save space settings.": "Failed to save space settings.",
+ "General": "General",
+ "Edit settings relating to your space.": "Edit settings relating to your space.",
+ "Saving...": "Saving...",
+ "Save Changes": "Save Changes",
+ "Leave Space": "Leave Space",
+ "Failed to update the visibility of this space": "Failed to update the visibility of this space",
+ "Failed to update the guest access of this space": "Failed to update the guest access of this space",
+ "Failed to update the history visibility of this space": "Failed to update the history visibility of this space",
+ "Hide advanced": "Hide advanced",
+ "Enable guest access": "Enable guest access",
+ "Guests can join a space without having an account.": "Guests can join a space without having an account.",
+ "This may be useful for public spaces.": "This may be useful for public spaces.",
+ "Show advanced": "Show advanced",
+ "Visibility": "Visibility",
+ "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.",
+ "anyone with the link can view and join": "anyone with the link can view and join",
+ "Invite only": "Invite only",
+ "only invited people can view and join": "only invited people can view and join",
+ "Preview Space": "Preview Space",
+ "Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
+ "Recommended for public spaces.": "Recommended for public spaces.",
"Settings": "Settings",
"Leave space": "Leave space",
"Create new room": "Create new room",
@@ -1035,6 +1067,8 @@
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
"Space options": "Space options",
+ "Expand": "Expand",
+ "Collapse": "Collapse",
"Remove": "Remove",
"This bridge was provisioned by .": "This bridge was provisioned by .",
"This bridge is managed by .": "This bridge is managed by .",
@@ -1221,8 +1255,6 @@
"Custom theme URL": "Custom theme URL",
"Add theme": "Add theme",
"Theme": "Theme",
- "Hide advanced": "Hide advanced",
- "Show advanced": "Show advanced",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
"Customise your appearance": "Customise your appearance",
@@ -1243,7 +1275,6 @@
"Deactivate Account": "Deactivate Account",
"Deactivate account": "Deactivate account",
"Discovery": "Discovery",
- "General": "General",
"Legal": "Legal",
"Credits": "Credits",
"For help with using %(brand)s, click here .": "For help with using %(brand)s, click here .",
@@ -1349,6 +1380,7 @@
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
"this room": "this room",
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
+ "Space information": "Space information",
"Room information": "Room information",
"Internal room ID:": "Internal room ID:",
"Room version": "Room version",
@@ -1674,14 +1706,18 @@
"Error removing address": "Error removing address",
"Main address": "Main address",
"not specified": "not specified",
+ "This space has no local addresses": "This space has no local addresses",
"This room has no local addresses": "This room has no local addresses",
"Local address": "Local address",
"Published Addresses": "Published Addresses",
- "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
+ "Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.",
+ "Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.",
+ "To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.",
"Other published addresses:": "Other published addresses:",
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
"Local Addresses": "Local Addresses",
+ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
"Show more": "Show more",
"Error updating flair": "Error updating flair",
@@ -1930,6 +1966,7 @@
"Error loading Widget": "Error loading Widget",
"Error - Mixed content": "Error - Mixed content",
"Popout widget": "Popout widget",
+ "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information",
"Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files",
"Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
@@ -2017,7 +2054,7 @@
"Room address": "Room address",
"e.g. my-room": "e.g. my-room",
"Some characters not allowed": "Some characters not allowed",
- "Please provide a room address": "Please provide a room address",
+ "Please provide an address": "Please provide an address",
"This address is available to use": "This address is available to use",
"This address is already in use": "This address is already in use",
"Server Options": "Server Options",
@@ -2027,7 +2064,6 @@
"Continue with %(provider)s": "Continue with %(provider)s",
"Sign in with single sign-on": "Sign in with single sign-on",
"And %(count)s more...|other": "And %(count)s more...",
- "Home": "Home",
"Enter a server name": "Enter a server name",
"Looks good": "Looks good",
"You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list",
@@ -2315,9 +2351,23 @@
"Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account .": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account .",
"Email (optional)": "Email (optional)",
"Please fill why you're reporting.": "Please fill why you're reporting.",
+ "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.",
+ "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.",
+ "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.",
+ "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.",
+ "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.",
+ "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.",
+ "Report Content": "Report Content",
+ "Disagree": "Disagree",
+ "Toxic Behaviour": "Toxic Behaviour",
+ "Illegal Content": "Illegal Content",
+ "Spam or propaganda": "Spam or propaganda",
+ "Report the entire room": "Report the entire room",
+ "Send report": "Send report",
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
- "Send report": "Send report",
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
"Failed to upgrade room": "Failed to upgrade room",
"The room upgrade could not be completed": "The room upgrade could not be completed",
@@ -2380,14 +2430,8 @@
"Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message",
"Command Help": "Command Help",
- "Failed to save space settings.": "Failed to save space settings.",
"Space settings": "Space settings",
- "Edit settings relating to your space.": "Edit settings relating to your space.",
- "Make this space private": "Make this space private",
- "Leave Space": "Leave Space",
- "View dev tools": "View dev tools",
- "Saving...": "Saving...",
- "Save Changes": "Save Changes",
+ "Settings - %(spaceName)s": "Settings - %(spaceName)s",
"To help us prevent this in future, please send us logs .": "To help us prevent this in future, please send us logs .",
"Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
@@ -2484,11 +2528,12 @@
"Share Message": "Share Message",
"Source URL": "Source URL",
"Collapse Reply Thread": "Collapse Reply Thread",
- "Report Content": "Report Content",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
"Set a new status...": "Set a new status...",
+ "Move up": "Move up",
+ "Move down": "Move down",
"View Community": "View Community",
"Unable to start audio streaming.": "Unable to start audio streaming.",
"Failed to start livestream": "Failed to start livestream",
@@ -2635,7 +2680,7 @@
"%(count)s messages deleted.|one": "%(count)s message deleted.",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
- "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
+ "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
@@ -2990,5 +3035,6 @@
"Esc": "Esc",
"Enter": "Enter",
"Space": "Space",
- "End": "End"
+ "End": "End",
+ "[number]": "[number]"
}
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index b6289969bd..c36f96f368 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -300,7 +300,7 @@ export default class EventIndex extends EventEmitter {
}
private eventToJson(ev: MatrixEvent) {
- const jsonEvent = ev.toJSON();
+ const jsonEvent: any = ev.toJSON();
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
if (ev.isEncrypted()) {
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 16950dc008..cc129fb6b9 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -112,7 +112,7 @@ export interface IVariables {
[key: string]: SubstitutionValue;
}
-type Tags = Record;
+export type Tags = Record;
export type TranslatedString = string | React.ReactNode;
diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts
index 21cd5d4cf7..89eec89500 100644
--- a/src/mjolnir/BanList.ts
+++ b/src/mjolnir/BanList.ts
@@ -92,7 +92,7 @@ export class BanList {
if (!room) return;
for (const eventType of ALL_RULE_TYPES) {
- const events = room.currentState.getStateEvents(eventType, undefined);
+ const events = room.currentState.getStateEvents(eventType);
for (const ev of events) {
if (!ev.getStateKey()) continue;
diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts
index effd9506f6..6cb193b1b1 100644
--- a/src/performance/entry-names.ts
+++ b/src/performance/entry-names.ts
@@ -37,17 +37,17 @@ export enum PerformanceEntryNames {
SWITCH_ROOM = "mx_SwithRoom",
JUMP_TO_ROOM = "mx_JumpToRoom",
- JOIN_ROOM = "mx_JoinRoom",
- CREATE_DM = "mx_CreateDM",
+ JOIN_ROOM = "mx_JoinRoom", // ✅
+ CREATE_DM = "mx_CreateDM", // ✅
PEEK_ROOM = "mx_PeekRoom",
/**
* User
*/
- VERIFY_E2EE_USER = "mx_VerifyE2EEUser",
- LOGIN = "mx_Login",
- REGISTER = "mx_Register",
+ VERIFY_E2EE_USER = "mx_VerifyE2EEUser", // ✅
+ LOGIN = "mx_Login", // ✅
+ REGISTER = "mx_Register", // ✅
/**
* VoIP
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index 08d8ccfd13..859fdf046a 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -263,7 +263,13 @@ function uint8ToString(buf: Buffer) {
return out;
}
-export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) {
+export async function submitFeedback(
+ endpoint: string,
+ label: string,
+ comment: string,
+ canContact = false,
+ extraData: Record = {},
+) {
let version = "UNKNOWN";
try {
version = await PlatformPeg.get().getAppVersion();
@@ -279,6 +285,10 @@ export async function submitFeedback(endpoint: string, label: string, comment: s
body.append("platform", PlatformPeg.get().getHumanReadableName());
body.append("user_id", MatrixClientPeg.get()?.getUserId());
+ for (const k in extraData) {
+ body.append(k, extraData[k]);
+ }
+
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
}
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 155d039572..6ed5e0c3d8 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -1,6 +1,6 @@
/*
Copyright 2017 Travis Ralston
-Copyright 2018, 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.
@@ -94,6 +94,9 @@ export interface ISetting {
[level: SettingLevel]: string;
};
+ // Optional description which will be shown as microCopy under SettingsFlags
+ description?: string;
+
// The supported levels are required. Preferably, use the preset arrays
// at the top of this file to define this rather than a custom array.
supportedLevels?: SettingLevel[];
@@ -127,10 +130,18 @@ export interface ISetting {
image: string; // require(...)
feedbackSubheading?: string;
feedbackLabel?: string;
+ extraSettings?: string[];
};
}
export const SETTINGS: {[setting: string]: ISetting} = {
+ "feature_report_to_moderators": {
+ isFeature: true,
+ displayName: _td("Report to moderators prototype. " +
+ "In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_spaces": {
isFeature: true,
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
@@ -167,8 +178,33 @@ export const SETTINGS: {[setting: string]: ISetting} = {
feedbackSubheading: _td("Your feedback will help make spaces better. " +
"The more detail you can go into, the better."),
feedbackLabel: "spaces-feedback",
+ extraSettings: [
+ "feature_spaces.all_rooms",
+ "feature_spaces.space_member_dms",
+ "feature_spaces.space_dm_badges",
+ ],
},
},
+ "feature_spaces.all_rooms": {
+ displayName: _td("Show all rooms in Home"),
+ supportedLevels: LEVELS_FEATURE,
+ default: true,
+ controller: new ReloadOnChangeController(),
+ },
+ "feature_spaces.space_member_dms": {
+ displayName: _td("Show people in spaces"),
+ description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " +
+ "If enabled, you'll automatically see everyone who is a member of the Space."),
+ supportedLevels: LEVELS_FEATURE,
+ default: true,
+ controller: new ReloadOnChangeController(),
+ },
+ "feature_spaces.space_dm_badges": {
+ displayName: _td("Show notification badges for People in Spaces"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ controller: new ReloadOnChangeController(),
+ },
"feature_dnd": {
isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"),
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index e1e300e185..44f3d5d838 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -248,6 +248,16 @@ export default class SettingsStore {
return _t(displayName as string);
}
+ /**
+ * Gets the translated description for a given setting
+ * @param {string} settingName The setting to look up.
+ * @return {String} The description for the setting, or null if not found.
+ */
+ public static getDescription(settingName: string) {
+ if (!SETTINGS[settingName]?.description) return null;
+ return _t(SETTINGS[settingName].description);
+ }
+
/**
* Determines if a setting is also a feature.
* @param {string} settingName The setting to look up.
diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts
index 023845c9ee..a6f4574a58 100644
--- a/src/stores/CommunityPrototypeStore.ts
+++ b/src/stores/CommunityPrototypeStore.ts
@@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient {
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
if (!pl) return this.isAdminOf(communityId);
+ const plContent = pl.getContent();
- const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
+ const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
return invitePl <= myMember.powerLevel;
}
@@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
if (data && data.getContent()) {
- return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url};
+ return {
+ displayName: data.getContent().name,
+ avatarMxc: data.getContent().avatar_url,
+ };
}
}
- return {displayName: room.name, avatarMxc: room.avatar_url};
+ return {
+ displayName: room.name,
+ avatarMxc: room.getMxcAvatarUrl(),
+ };
}
protected async onReady(): Promise {
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index cc3eafffcd..87978df471 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -276,7 +276,7 @@ class RoomViewStore extends Store {
const address = this.state.roomAlias || this.state.roomId;
const viaServers = this.state.viaServers || [];
try {
- await retry(() => cli.joinRoom(address, {
+ await retry(() => cli.joinRoom(address, {
viaServers,
...payload.opts,
}), NUM_JOIN_RETRY, (err) => {
diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.ts
similarity index 73%
rename from src/stores/SetupEncryptionStore.js
rename to src/stores/SetupEncryptionStore.ts
index b768ae69df..88385d0399 100644
--- a/src/stores/SetupEncryptionStore.js
+++ b/src/stores/SetupEncryptionStore.ts
@@ -15,29 +15,42 @@ limitations under the License.
*/
import EventEmitter from 'events';
+import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
+import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup";
+import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
-export const PHASE_LOADING = 0;
-export const PHASE_INTRO = 1;
-export const PHASE_BUSY = 2;
-export const PHASE_DONE = 3; //final done stage, but still showing UX
-export const PHASE_CONFIRM_SKIP = 4;
-export const PHASE_FINISHED = 5; //UX can be closed
+export enum Phase {
+ Loading = 0,
+ Intro = 1,
+ Busy = 2,
+ Done = 3, // final done stage, but still showing UX
+ ConfirmSkip = 4,
+ Finished = 5, // UX can be closed
+}
export class SetupEncryptionStore extends EventEmitter {
- static sharedInstance() {
- if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
- return global.mx_SetupEncryptionStore;
+ private started: boolean;
+ public phase: Phase;
+ public verificationRequest: VerificationRequest;
+ public backupInfo: IKeyBackupVersion;
+ public keyId: string;
+ public keyInfo: ISecretStorageKeyInfo;
+ public hasDevicesToVerifyAgainst: boolean;
+
+ public static sharedInstance() {
+ if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore();
+ return window.mxSetupEncryptionStore;
}
- start() {
- if (this._started) {
+ public start(): void {
+ if (this.started) {
return;
}
- this._started = true;
- this.phase = PHASE_LOADING;
+ this.started = true;
+ this.phase = Phase.Loading;
this.verificationRequest = null;
this.backupInfo = null;
@@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter {
const cli = MatrixClientPeg.get();
cli.on("crypto.verification.request", this.onVerificationRequest);
- cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
+ cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged);
const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
if (requestsInProgress.length) {
// If there are multiple, we take the most recent. Equally if the user sends another request from
// another device after this screen has been shown, we'll switch to the new one, so this
// generally doesn't support multiple requests.
- this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
+ this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
}
this.fetchKeyInfo();
}
- stop() {
- if (!this._started) {
+ public stop(): void {
+ if (!this.started) {
return;
}
- this._started = false;
+ this.started = false;
if (this.verificationRequest) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
- MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
+ MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
}
}
- async fetchKeyInfo() {
+ public async fetchKeyInfo(): Promise {
const cli = MatrixClientPeg.get();
const keys = await cli.isSecretStored('m.cross_signing.master', false);
if (keys === null || Object.keys(keys).length === 0) {
@@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter {
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
// skip before we can even render anything.
- this.phase = PHASE_FINISHED;
+ this.phase = Phase.Finished;
} else {
- this.phase = PHASE_INTRO;
+ this.phase = Phase.Intro;
}
this.emit("update");
}
- async usePassPhrase() {
- this.phase = PHASE_BUSY;
+ public async usePassPhrase(): Promise {
+ this.phase = Phase.Busy;
this.emit("update");
const cli = MatrixClientPeg.get();
try {
@@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter {
// passphase cached for that work. This dialog itself will only wait
// on the first trust check, and the key backup restore will happen
// in the background.
- await new Promise((resolve, reject) => {
+ await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => {
accessSecretStorage(async () => {
await cli.checkOwnCrossSigningTrust();
resolve();
@@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter {
});
if (cli.getCrossSigningId()) {
- this.phase = PHASE_DONE;
+ this.phase = Phase.Done;
this.emit("update");
}
} catch (e) {
@@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
- this.phase = PHASE_INTRO;
+ this.phase = Phase.Intro;
this.emit("update");
}
}
- _onUserTrustStatusChanged = (userId) => {
+ private onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
if (publicKeysTrusted) {
- this.phase = PHASE_DONE;
+ this.phase = Phase.Done;
this.emit("update");
}
}
- onVerificationRequest = (request) => {
- this._setActiveVerificationRequest(request);
+ public onVerificationRequest = (request: VerificationRequest): void => {
+ this.setActiveVerificationRequest(request);
}
- onVerificationRequestChange = () => {
+ public onVerificationRequestChange = (): void => {
if (this.verificationRequest.cancelled) {
this.verificationRequest.off("change", this.onVerificationRequestChange);
this.verificationRequest = null;
@@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter {
// cross signing to be ready to use, so wait for the user trust status to
// change (or change to DONE if it's already ready).
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
- this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY;
+ this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy;
this.emit("update");
}
}
- skip() {
- this.phase = PHASE_CONFIRM_SKIP;
+ public skip(): void {
+ this.phase = Phase.ConfirmSkip;
this.emit("update");
}
- skipConfirm() {
- this.phase = PHASE_FINISHED;
+ public skipConfirm(): void {
+ this.phase = Phase.Finished;
this.emit("update");
}
- returnAfterSkip() {
- this.phase = PHASE_INTRO;
+ public returnAfterSkip(): void {
+ this.phase = Phase.Intro;
this.emit("update");
}
- done() {
- this.phase = PHASE_FINISHED;
+ public done(): void {
+ this.phase = Phase.Finished;
this.emit("update");
// async - ask other clients for keys, if necessary
MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
}
- async _setActiveVerificationRequest(request) {
+ private async setActiveVerificationRequest(request: VerificationRequest): Promise {
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
if (this.verificationRequest) {
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index 40997d30a8..e498574467 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -14,36 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {ListIteratee, Many, sortBy, throttle} from "lodash";
-import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import { ListIteratee, Many, sortBy, throttle } from "lodash";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
+import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
-import {ActionPayload} from "../dispatcher/payloads";
+import { ActionPayload } from "../dispatcher/payloads";
import RoomListStore from "./room-list/RoomListStore";
import SettingsStore from "../settings/SettingsStore";
import DMRoomMap from "../utils/DMRoomMap";
-import {FetchRoomFn} from "./notifications/ListNotificationState";
-import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
-import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
-import {DefaultTagID} from "./room-list/models";
-import {EnhancedMap, mapDiff} from "../utils/maps";
-import {setHasDiff} from "../utils/sets";
-import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
+import { FetchRoomFn } from "./notifications/ListNotificationState";
+import { SpaceNotificationState } from "./notifications/SpaceNotificationState";
+import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
+import { DefaultTagID } from "./room-list/models";
+import { EnhancedMap, mapDiff } from "../utils/maps";
+import { setHasDiff } from "../utils/sets";
+import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore";
+import { Action } from "../dispatcher/actions";
+import { arrayHasDiff } from "../utils/arrays";
+import { objectDiff } from "../utils/objects";
+import { arrayHasOrderChange } from "../utils/arrays";
+import { reorderLexicographically } from "../utils/stringOrderField";
+
+type SpaceKey = string | symbol;
interface IState {}
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
+export const HOME_SPACE = Symbol("home-space");
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
-// Space Room ID will be emitted when a Space's children change
+// Space Room ID/HOME_SPACE will be emitted when a Space's children change
export interface ISuggestedRoom extends ISpaceSummaryRoom {
viaServers: string[];
@@ -51,7 +59,8 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom {
const MAX_SUGGESTED_ROOMS = 20;
-const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
+const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE";
+const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@@ -60,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
}, [[], []]);
};
-// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
-export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => {
- let validatedOrder: string = null;
-
- if (typeof order === "string" && Array.from(order).every((c: string) => {
+const validOrder = (order: string): string | undefined => {
+ if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7E;
})) {
- validatedOrder = order;
+ return order;
}
+};
- return [validatedOrder, creationTs, roomId];
+// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
+export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => {
+ return [validOrder(order), creationTs, roomId];
}
const getRoomFn: FetchRoomFn = (room: Room) => {
@@ -85,16 +94,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// The spaces representing the roots of the various tree-like hierarchies
private rootSpaces: Room[] = [];
+ // The list of rooms not present in any currently joined spaces
+ private orphanedRooms = new Set();
// Map from room ID to set of spaces which list it as a child
private parentMap = new EnhancedMap>();
- // Map from spaceId to SpaceNotificationState instance representing that space
- private notificationStateMap = new Map();
+ // Map from SpaceKey to SpaceNotificationState instance representing that space
+ private notificationStateMap = new Map();
// Map from space key to Set of room IDs that should be shown as part of that space's filter
- private spaceFilteredRooms = new Map>();
- // The space currently selected in the Space Panel - if null then All Rooms is selected
+ private spaceFilteredRooms = new Map>();
+ // The space currently selected in the Space Panel - if null then Home is selected
private _activeSpace?: Room = null;
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set();
+ private spaceOrderLocalEchoMap = new Map();
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
@@ -133,7 +145,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
- if (space?.getMyMembership !== "invite" &&
+ if (space?.getMyMembership() !== "invite" &&
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
) {
defaultDispatcher.dispatch({
@@ -214,7 +226,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomId = ev.getStateKey();
const childRoom = this.matrixClient?.getRoom(roomId);
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
- return getOrder(ev.getContent().order, createTs, roomId);
+ return getChildOrder(ev.getContent().order, createTs, roomId);
}).map(ev => {
return this.matrixClient.getRoom(ev.getStateKey());
}).filter(room => {
@@ -251,10 +263,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
public getSpaceFilteredRoomIds = (space: Room | null): Set => {
- if (!space) {
+ if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
- return this.spaceFilteredRooms.get(space.roomId) || new Set();
+ return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
};
private rebuild = throttle(() => {
@@ -285,7 +297,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
});
});
- const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
+ const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
// somewhat algorithm to handle full-cycles
const detachedNodes = new Set(spaces);
@@ -326,7 +338,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// rootSpaces.push(space);
// });
- this.rootSpaces = rootSpaces;
+ this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
+ this.rootSpaces = this.sortRootSpaces(rootSpaces);
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
@@ -338,14 +351,34 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
- this._invitedSpaces = new Set(invitedSpaces);
+ this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
}, 100, {trailing: true, leading: true});
- onSpaceUpdate = () => {
+ private onSpaceUpdate = () => {
this.rebuild();
}
+ private showInHomeSpace = (room: Room) => {
+ if (SettingsStore.getValue("feature_spaces.all_rooms")) return true;
+ if (room.isSpaceRoom()) return false;
+ return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
+ || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
+ || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
+ };
+
+ // Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
+ // This can only change whether it shows up in the HOME_SPACE or not
+ private onRoomUpdate = (room: Room) => {
+ if (this.showInHomeSpace(room)) {
+ this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
+ this.emit(HOME_SPACE);
+ } else if (!this.orphanedRooms.has(room.roomId)) {
+ this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
+ this.emit(HOME_SPACE);
+ }
+ };
+
private onSpaceMembersChange = (ev: MatrixEvent) => {
// skip this update if we do not have a DM with this user
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
@@ -359,6 +392,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ // put all room invites in the Home Space
+ const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
+ this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId)));
+
+ visibleRooms.forEach(room => {
+ if (this.showInHomeSpace(room)) {
+ this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
+ }
+ });
+ }
+
this.rootSpaces.forEach(s => {
// traverse each space tree in DFS to build up the supersets as you go up,
// reusing results from like subtrees.
@@ -374,13 +419,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomIds = new Set(childRooms.map(r => r.roomId));
const space = this.matrixClient?.getRoom(spaceId);
- // Add relevant DMs
- space?.getMembers().forEach(member => {
- if (member.membership !== "join" && member.membership !== "invite") return;
- DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
- roomIds.add(roomId);
+ if (SettingsStore.getValue("feature_spaces.space_member_dms")) {
+ // Add relevant DMs
+ space?.getMembers().forEach(member => {
+ if (member.membership !== "join" && member.membership !== "invite") return;
+ DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
+ roomIds.add(roomId);
+ });
});
- });
+ }
const newPath = new Set(parentPath).add(spaceId);
childSpaces.forEach(childSpace => {
@@ -406,6 +453,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// Update NotificationStates
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
if (roomIds.has(room.roomId)) {
+ if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true;
+
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
}
@@ -423,8 +472,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId));
}
if (!parent) {
- const parents = Array.from(this.parentMap.get(roomId) || []);
- parent = parents.find(p => this.matrixClient.getRoom(p));
+ const parentIds = Array.from(this.parentMap.get(roomId) || []);
+ for (const parentId of parentIds) {
+ const room = this.matrixClient.getRoom(parentId);
+ if (room) {
+ parent = room;
+ break;
+ }
+ }
}
// don't trigger a context switch when we are switching a space to match the chosen room
@@ -472,6 +527,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ private notifyIfOrderChanged(): void {
+ const rootSpaces = this.sortRootSpaces(this.rootSpaces);
+ if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
+ this.rootSpaces = rootSpaces;
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+ }
+ }
+
private onRoomState = (ev: MatrixEvent) => {
const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return;
@@ -489,6 +552,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// TODO confirm this after implementing parenting behaviour
if (room.isSpaceRoom()) {
this.onSpaceUpdate();
+ } else if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.onRoomUpdate(room);
}
this.emit(room.roomId);
break;
@@ -501,8 +566,47 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
+ if (!room.isSpaceRoom()) return;
+
+ if (ev.getType() === EventType.SpaceOrder) {
+ this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
+ const order = ev.getContent()?.order;
+ const lastOrder = lastEv?.getContent()?.order;
+ if (order !== lastOrder) {
+ this.notifyIfOrderChanged();
+ }
+ } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
+ // If the room was in favourites and now isn't or the opposite then update its position in the trees
+ const oldTags = lastEv?.getContent()?.tags || {};
+ const newTags = ev.getContent()?.tags || {};
+ if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
+ this.onRoomUpdate(room);
+ }
+ }
+ }
+
+ private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
+ if (ev.getType() === EventType.Direct) {
+ const lastContent = lastEvent.getContent();
+ const content = ev.getContent();
+
+ const diff = objectDiff>(lastContent, content);
+ // filter out keys which changed by reference only by checking whether the sets differ
+ const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
+ // DM tag changes, refresh relevant rooms
+ new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
+ const room = this.matrixClient?.getRoom(roomId);
+ if (room) {
+ this.onRoomUpdate(room);
+ }
+ });
+ }
+ };
+
protected async reset() {
this.rootSpaces = [];
+ this.orphanedRooms = new Set();
this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map();
@@ -516,7 +620,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (this.matrixClient) {
this.matrixClient.removeListener("Room", this.onRoom);
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
+ this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.matrixClient.removeListener("accountData", this.onAccountData);
+ }
}
await this.reset();
}
@@ -525,7 +633,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (!SettingsStore.getValue("feature_spaces")) return;
this.matrixClient.on("Room", this.onRoom);
this.matrixClient.on("Room.myMembership", this.onRoom);
+ this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.matrixClient.on("accountData", this.onAccountData);
+ }
await this.onSpaceUpdate(); // trigger an initial update
@@ -550,7 +662,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// Don't context switch when navigating to the space room
// as it will cause you to end up in the wrong room
this.setActiveSpace(room, false);
- } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
+ } else if (
+ (!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) &&
+ !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
+ ) {
this.switchToRelatedSpace(roomId);
}
@@ -565,10 +680,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.setActiveSpace(null, false);
}
break;
+ case Action.SwitchSpace:
+ if (payload.num === 0) {
+ this.setActiveSpace(null);
+ } else if (this.spacePanelSpaces.length >= payload.num) {
+ this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
+ }
}
}
- public getNotificationState(key: string): SpaceNotificationState {
+ public getNotificationState(key: SpaceKey): SpaceNotificationState {
if (this.notificationStateMap.has(key)) {
return this.notificationStateMap.get(key);
}
@@ -599,6 +720,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
}
+
+ private getSpaceTagOrdering = (space: Room): string | undefined => {
+ if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
+ return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
+ };
+
+ private sortRootSpaces(spaces: Room[]): Room[] {
+ return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]);
+ }
+
+ private async setRootSpaceOrder(space: Room, order: string): Promise {
+ this.spaceOrderLocalEchoMap.set(space.roomId, order);
+ try {
+ await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order });
+ } catch (e) {
+ console.warn("Failed to set root space order", e);
+ if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) {
+ this.spaceOrderLocalEchoMap.delete(space.roomId);
+ }
+ }
+ }
+
+ public moveRootSpace(fromIndex: number, toIndex: number): void {
+ const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering);
+ const changes = reorderLexicographically(currentOrders, fromIndex, toIndex);
+
+ changes.forEach(({ index, order }) => {
+ this.setRootSpaceOrder(this.rootSpaces[index], order);
+ });
+
+ this.notifyIfOrderChanged();
+ }
}
export default class SpaceStore {
diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts
index 09120d6108..0b0be50541 100644
--- a/src/stores/WidgetEchoStore.ts
+++ b/src/stores/WidgetEchoStore.ts
@@ -16,8 +16,8 @@ limitations under the License.
import EventEmitter from 'events';
import { IWidget } from 'matrix-widget-api';
-import MatrixEvent from "matrix-js-sdk/src/models/event";
-import {WidgetType} from "../widgets/WidgetType";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { WidgetType } from "../widgets/WidgetType";
/**
* Acts as a place to get & set widget state, storing local echo state and
diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
index 0b1b78bc75..a1f7786578 100644
--- a/src/stores/room-list/SpaceWatcher.ts
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
+import SettingsStore from "../../settings/SettingsStore";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
@@ -28,6 +29,11 @@ export class SpaceWatcher {
private activeSpace: Room = SpaceStore.instance.activeSpace;
constructor(private store: RoomListStoreClass) {
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.filter = new SpaceFilterCondition();
+ this.updateFilter();
+ store.addFilter(this.filter);
+ }
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
}
@@ -35,7 +41,7 @@ export class SpaceWatcher {
this.activeSpace = activeSpace;
if (this.filter) {
- if (activeSpace) {
+ if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) {
this.updateFilter();
} else {
this.store.removeFilter(this.filter);
@@ -49,9 +55,11 @@ export class SpaceWatcher {
};
private updateFilter = () => {
- SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
- this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
- });
+ if (this.activeSpace) {
+ SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
+ this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
+ });
+ }
this.filter.updateSpace(this.activeSpace);
};
}
diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts
index 6a06bee0d8..0e6965d843 100644
--- a/src/stores/room-list/filters/SpaceFilterCondition.ts
+++ b/src/stores/room-list/filters/SpaceFilterCondition.ts
@@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable";
-import SpaceStore from "../../SpaceStore";
+import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
import { setHasDiff } from "../../../utils/sets";
/**
@@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets";
* + All DMs
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
- private roomIds = new Set();
+ private roomIds = new Set();
private space: Room = null;
public get kind(): FilterKind {
@@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
}
};
- private getSpaceEventKey = (space: Room) => space.roomId;
+ private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
public updateSpace(space: Room) {
- if (this.space) {
- SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
- }
+ SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space
}
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 397d637125..6dcaf7abd7 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -51,7 +51,7 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import {getCustomTheme} from "../../theme";
import CountlyAnalytics from "../../CountlyAnalytics";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
-import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event";
import { ELEMENT_CLIENT_ID } from "../../identifiers";
import { getUserLanguage } from "../../languageHandler";
@@ -415,7 +415,7 @@ export class StopGapWidget extends EventEmitter {
private feedEvent(ev: MatrixEvent) {
if (!this.messaging) return;
- const raw = ev.event;
+ const raw = ev.event as IEvent;
this.messaging.feedEvent(raw).catch(e => {
console.error("Error sending event to widget: ", e);
});
diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts
index 25e81c47a2..5218e4a0bc 100644
--- a/src/stores/widgets/StopGapWidgetDriver.ts
+++ b/src/stores/widgets/StopGapWidgetDriver.ts
@@ -145,7 +145,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
return {roomId, eventId: r.event_id};
}
- public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise {
+ public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise {
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
const client = MatrixClientPeg.get();
@@ -167,9 +167,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
return results.map(e => e.event);
}
- public async readStateEvents(
- eventType: string, stateKey: string | undefined, limit: number,
- ): Promise {
+ public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise {
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
const client = MatrixClientPeg.get();
@@ -178,7 +176,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
const results: MatrixEvent[] = [];
- const state = room.currentState.events.get(eventType);
+ const state: Map = room.currentState.events.get(eventType);
if (state) {
if (stateKey === "" || !!stateKey) {
const forKey = state.get(stateKey);
diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts
index c856d39d1f..05425b93c0 100644
--- a/src/toasts/UnverifiedSessionToast.ts
+++ b/src/toasts/UnverifiedSessionToast.ts
@@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener';
import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast";
import { Action } from "../dispatcher/actions";
-import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
+import { UserTab } from "../components/views/dialogs/UserSettingsDialog";
function toastKey(deviceId: string) {
return "unverified_session_" + deviceId;
@@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
dis.dispatch({
action: Action.ViewUserSettings,
- initialTabId: USER_SECURITY_TAB,
+ initialTabId: UserTab.Security,
});
};
diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts
index b166674043..3e554f145d 100644
--- a/src/utils/DMRoomMap.ts
+++ b/src/utils/DMRoomMap.ts
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {MatrixClientPeg} from '../MatrixClientPeg';
-import {uniq} from "lodash";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {Event} from "matrix-js-sdk/src/models/event";
-import {MatrixClient} from "matrix-js-sdk/src/client";
+import { uniq } from "lodash";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+
+import { MatrixClientPeg } from '../MatrixClientPeg';
/**
* Class that takes a Matrix Client and flips the m.direct map
@@ -30,15 +30,13 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
export default class DMRoomMap {
private static sharedInstance: DMRoomMap;
- private matrixClient: MatrixClient;
// TODO: convert these to maps
private roomToUser: {[key: string]: string} = null;
private userToRooms: {[key: string]: string[]} = null;
private hasSentOutPatchDirectAccountDataPatch: boolean;
- private mDirectEvent: Event;
+ private mDirectEvent: object;
- constructor(matrixClient) {
- this.matrixClient = matrixClient;
+ constructor(private readonly matrixClient: MatrixClient) {
// see onAccountData
this.hasSentOutPatchDirectAccountDataPatch = false;
diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.ts
similarity index 52%
rename from src/utils/EditorStateTransfer.js
rename to src/utils/EditorStateTransfer.ts
index c7782a9ea8..ba303f9b73 100644
--- a/src/utils/EditorStateTransfer.js
+++ b/src/utils/EditorStateTransfer.ts
@@ -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.
@@ -14,36 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
+import { SerializedPart } from "../editor/parts";
+import { Caret } from "../editor/caret";
+
/**
* Used while editing, to pass the event, and to preserve editor state
* from one editor instance to another when remounting the editor
* upon receiving the remote echo for an unsent event.
*/
export default class EditorStateTransfer {
- constructor(event) {
- this._event = event;
- this._serializedParts = null;
- this.caret = null;
+ private serializedParts: SerializedPart[] = null;
+ private caret: Caret = null;
+
+ constructor(private readonly event: MatrixEvent) {}
+
+ public setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
+ this.caret = caret;
+ this.serializedParts = serializedParts;
}
- setEditorState(caret, serializedParts) {
- this._caret = caret;
- this._serializedParts = serializedParts;
+ public hasEditorState() {
+ return !!this.serializedParts;
}
- hasEditorState() {
- return !!this._serializedParts;
+ public getSerializedParts() {
+ return this.serializedParts;
}
- getSerializedParts() {
- return this._serializedParts;
+ public getCaret() {
+ return this.caret;
}
- getCaret() {
- return this._caret;
- }
-
- getEvent() {
- return this._event;
+ public getEvent() {
+ return this.event;
}
}
diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.tsx
similarity index 84%
rename from src/utils/ErrorUtils.js
rename to src/utils/ErrorUtils.tsx
index b5bd5b0af0..c39ee21f09 100644
--- a/src/utils/ErrorUtils.js
+++ b/src/utils/ErrorUtils.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
+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.
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { _t, _td } from '../languageHandler';
+import React, { ReactNode } from "react";
+import { MatrixError } from "matrix-js-sdk/src/http-api";
+
+import { _t, _td, Tags, TranslatedString } from '../languageHandler';
/**
* Produce a translated error message for a
@@ -30,7 +33,12 @@ import { _t, _td } from '../languageHandler';
* for any tags in the strings apart from 'a'
* @returns {*} Translated string or react component
*/
-export function messageForResourceLimitError(limitType, adminContact, strings, extraTranslations) {
+export function messageForResourceLimitError(
+ limitType: string,
+ adminContact: string,
+ strings: Record,
+ extraTranslations?: Tags,
+): TranslatedString {
let errString = strings[limitType];
if (errString === undefined) errString = strings[''];
@@ -49,7 +57,7 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
}
}
-export function messageForSyncError(err) {
+export function messageForSyncError(err: MatrixError | Error): ReactNode {
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const limitError = messageForResourceLimitError(
err.data.limit_type,
diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.ts
similarity index 85%
rename from src/utils/EventUtils.js
rename to src/utils/EventUtils.ts
index be21896417..3d9c60d9cd 100644
--- a/src/utils/EventUtils.js
+++ b/src/utils/EventUtils.ts
@@ -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,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { EventStatus } from 'matrix-js-sdk/src/models/event';
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
+
+import { MatrixClientPeg } from '../MatrixClientPeg';
import shouldHideEvent from "../shouldHideEvent";
+
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
* which effectively checks whether it's a regular message that has been sent and that we
@@ -25,7 +28,7 @@ import shouldHideEvent from "../shouldHideEvent";
* @param {MatrixEvent} mxEvent The event to check
* @returns {boolean} true if actionable
*/
-export function isContentActionable(mxEvent) {
+export function isContentActionable(mxEvent: MatrixEvent): boolean {
const { status: eventStatus } = mxEvent;
// status is SENT before remote-echo, null after
@@ -45,7 +48,7 @@ export function isContentActionable(mxEvent) {
return false;
}
-export function canEditContent(mxEvent) {
+export function canEditContent(mxEvent: MatrixEvent): boolean {
if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) {
return false;
}
@@ -56,7 +59,7 @@ export function canEditContent(mxEvent) {
mxEvent.getSender() === MatrixClientPeg.get().getUserId();
}
-export function canEditOwnEvent(mxEvent) {
+export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
// for now we only allow editing
// your own events. So this just call through
// In the future though, moderators will be able to
@@ -67,7 +70,7 @@ export function canEditOwnEvent(mxEvent) {
}
const MAX_JUMP_DISTANCE = 100;
-export function findEditableEvent(room, isForward, fromEventId = undefined) {
+export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
const liveTimeline = room.getLiveTimeline();
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
const maxIdx = events.length - 1;
diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.ts
similarity index 82%
rename from src/utils/IdentityServerUtils.js
rename to src/utils/IdentityServerUtils.ts
index 5ece308954..2476adca19 100644
--- a/src/utils/IdentityServerUtils.js
+++ b/src/utils/IdentityServerUtils.ts
@@ -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.
@@ -15,14 +15,15 @@ limitations under the License.
*/
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
+
import SdkConfig from '../SdkConfig';
import {MatrixClientPeg} from '../MatrixClientPeg';
-export function getDefaultIdentityServerUrl() {
+export function getDefaultIdentityServerUrl(): string {
return SdkConfig.get()['validated_server_config']['isUrl'];
}
-export function useDefaultIdentityServer() {
+export function useDefaultIdentityServer(): void {
const url = getDefaultIdentityServerUrl();
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
@@ -30,7 +31,7 @@ export function useDefaultIdentityServer() {
});
}
-export async function doesIdentityServerHaveTerms(fullUrl) {
+export async function doesIdentityServerHaveTerms(fullUrl: string): Promise {
let terms;
try {
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
@@ -46,7 +47,7 @@ export async function doesIdentityServerHaveTerms(fullUrl) {
return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0);
}
-export function doesAccountDataHaveIdentityServer() {
+export function doesAccountDataHaveIdentityServer(): boolean {
const event = MatrixClientPeg.get().getAccountData("m.identity_server");
return event && event.getContent() && event.getContent()['base_url'];
}
diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.tsx
similarity index 84%
rename from src/utils/MessageDiffUtils.js
rename to src/utils/MessageDiffUtils.tsx
index 7398173fdd..b5d5e31432 100644
--- a/src/utils/MessageDiffUtils.js
+++ b/src/utils/MessageDiffUtils.tsx
@@ -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.
@@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { ReactNode } from 'react';
import classNames from 'classnames';
-import DiffMatchPatch from 'diff-match-patch';
-import {DiffDOM} from "diff-dom";
-import { checkBlockNode, bodyToHtml } from "../HtmlUtils";
+import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
+import { Action, DiffDOM, IDiff } from "diff-dom";
+import { IContent } from "matrix-js-sdk/src/models/event";
+
+import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
const decodeEntities = (function() {
let textarea = null;
- return function(string) {
+ return function(str: string): string {
if (!textarea) {
textarea = document.createElement("textarea");
}
- textarea.innerHTML = string;
+ textarea.innerHTML = str;
return textarea.value;
};
})();
-function textToHtml(text) {
+function textToHtml(text: string): string {
const container = document.createElement("div");
container.textContent = text;
return container.innerHTML;
}
-function getSanitizedHtmlBody(content) {
- const opts = {
+function getSanitizedHtmlBody(content: IContent): string {
+ const opts: IOptsReturnString = {
stripReplyFallback: true,
returnString: true,
};
@@ -57,21 +59,21 @@ function getSanitizedHtmlBody(content) {
}
}
-function wrapInsertion(child) {
+function wrapInsertion(child: Node): HTMLElement {
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
wrapper.className = "mx_EditHistoryMessage_insertion";
wrapper.appendChild(child);
return wrapper;
}
-function wrapDeletion(child) {
+function wrapDeletion(child: Node): HTMLElement {
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
wrapper.className = "mx_EditHistoryMessage_deletion";
wrapper.appendChild(child);
return wrapper;
}
-function findRefNodes(root, route, isAddition) {
+function findRefNodes(root: Node, route: number[], isAddition = false) {
let refNode = root;
let refParentNode;
const end = isAddition ? route.length - 1 : route.length;
@@ -79,7 +81,7 @@ function findRefNodes(root, route, isAddition) {
refParentNode = refNode;
refNode = refNode.childNodes[route[i]];
}
- return {refNode, refParentNode};
+ return { refNode, refParentNode };
}
function diffTreeToDOM(desc) {
@@ -101,7 +103,7 @@ function diffTreeToDOM(desc) {
}
}
-function insertBefore(parent, nextSibling, child) {
+function insertBefore(parent: Node, nextSibling: Node | null, child: Node): void {
if (nextSibling) {
parent.insertBefore(child, nextSibling);
} else {
@@ -109,7 +111,7 @@ function insertBefore(parent, nextSibling, child) {
}
}
-function isRouteOfNextSibling(route1, route2) {
+function isRouteOfNextSibling(route1: number[], route2: number[]): boolean {
// routes are arrays with indices,
// to be interpreted as a path in the dom tree
@@ -127,7 +129,7 @@ function isRouteOfNextSibling(route1, route2) {
return route2[lastD1Idx] >= route1[lastD1Idx];
}
-function adjustRoutes(diff, remainingDiffs) {
+function adjustRoutes(diff: IDiff, remainingDiffs: IDiff[]): void {
if (diff.action === "removeTextElement" || diff.action === "removeElement") {
// as removed text is not removed from the html, but marked as deleted,
// we need to readjust indices that assume the current node has been removed.
@@ -140,14 +142,14 @@ function adjustRoutes(diff, remainingDiffs) {
}
}
-function stringAsTextNode(string) {
+function stringAsTextNode(string: string): Text {
return document.createTextNode(decodeEntities(string));
}
-function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
+function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
switch (diff.action) {
- case "replaceElement": {
+ case Action.ReplaceElement: {
const container = document.createElement("span");
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
@@ -156,22 +158,22 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
refNode.parentNode.replaceChild(container, refNode);
break;
}
- case "removeTextElement": {
+ case Action.RemoveTextElement: {
const delNode = wrapDeletion(stringAsTextNode(diff.value));
refNode.parentNode.replaceChild(delNode, refNode);
break;
}
- case "removeElement": {
+ case Action.RemoveElement: {
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
refNode.parentNode.replaceChild(delNode, refNode);
break;
}
- case "modifyTextElement": {
+ case Action.ModifyTextElement: {
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
diffMathPatch.diff_cleanupSemantic(textDiffs);
const container = document.createElement("span");
for (const [modifier, text] of textDiffs) {
- let textDiffNode = stringAsTextNode(text);
+ let textDiffNode: Node = stringAsTextNode(text);
if (modifier < 0) {
textDiffNode = wrapDeletion(textDiffNode);
} else if (modifier > 0) {
@@ -182,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
refNode.parentNode.replaceChild(container, refNode);
break;
}
- case "addElement": {
+ case Action.AddElement: {
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
insertBefore(refParentNode, refNode, insNode);
break;
}
- case "addTextElement": {
+ case Action.AddTextElement: {
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
// but we must insert the node anyway so that we don't break the route child IDs.
// See https://github.com/fiduswriter/diffDOM/issues/100
@@ -197,11 +199,11 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
}
// e.g. when changing a the href of a link,
// show the link with old href as removed and with the new href as added
- case "removeAttribute":
- case "addAttribute":
- case "modifyAttribute": {
+ case Action.RemoveAttribute:
+ case Action.AddAttribute:
+ case Action.ModifyAttribute: {
const delNode = wrapDeletion(refNode.cloneNode(true));
- const updatedNode = refNode.cloneNode(true);
+ const updatedNode = refNode.cloneNode(true) as HTMLElement;
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
updatedNode.setAttribute(diff.name, diff.newValue);
} else {
@@ -220,12 +222,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
}
}
-function routeIsEqual(r1, r2) {
+function routeIsEqual(r1: number[], r2: number[]): boolean {
return r1.length === r2.length && !r1.some((e, i) => e !== r2[i]);
}
// workaround for https://github.com/fiduswriter/diffDOM/issues/90
-function filterCancelingOutDiffs(originalDiffActions) {
+function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] {
const diffActions = originalDiffActions.slice();
for (let i = 0; i < diffActions.length; ++i) {
@@ -252,7 +254,7 @@ function filterCancelingOutDiffs(originalDiffActions) {
* @param {object} editContent the content for the edit message
* @return {object} a react element similar to what `bodyToHtml` returns
*/
-export function editBodyDiffToHtml(originalContent, editContent) {
+export function editBodyDiffToHtml(originalContent: IContent, editContent: IContent): ReactNode {
// wrap the body in a div, DiffDOM needs a root element
const originalBody = `${getSanitizedHtmlBody(originalContent)}
`;
const editBody = `${getSanitizedHtmlBody(editContent)}
`;
diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.ts
similarity index 89%
rename from src/utils/PinningUtils.js
rename to src/utils/PinningUtils.ts
index 90d26cc988..ec1eeccf1f 100644
--- a/src/utils/PinningUtils.js
+++ b/src/utils/PinningUtils.ts
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
export default class PinningUtils {
/**
* Determines if the given event may be pinned.
* @param {MatrixEvent} event The event to check.
* @return {boolean} True if the event may be pinned, false otherwise.
*/
- static isPinnable(event) {
+ static isPinnable(event: MatrixEvent): boolean {
if (!event) return false;
if (event.getType() !== "m.room.message") return false;
if (event.isRedacted()) return false;
diff --git a/src/utils/Receipt.js b/src/utils/Receipt.ts
similarity index 83%
rename from src/utils/Receipt.js
rename to src/utils/Receipt.ts
index d88c67fb18..2a626decc4 100644
--- a/src/utils/Receipt.js
+++ b/src/utils/Receipt.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
+Copyright 2016 - 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,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
/**
* Given MatrixEvent containing receipts, return the first
* read receipt from the given user ID, or null if no such
@@ -23,7 +25,7 @@ limitations under the License.
* @param {string} userId A user ID
* @returns {Object} Read receipt
*/
-export function findReadReceiptFromUserId(receiptEvent, userId) {
+export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: string): object | null {
const receiptKeys = Object.keys(receiptEvent.getContent());
for (let i = 0; i < receiptKeys.length; ++i) {
const rcpt = receiptEvent.getContent()[receiptKeys[i]];
diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.ts
similarity index 62%
rename from src/utils/ResizeNotifier.js
rename to src/utils/ResizeNotifier.ts
index 4d46d10f6c..8bb7f52e57 100644
--- a/src/utils/ResizeNotifier.js
+++ b/src/utils/ResizeNotifier.ts
@@ -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.
@@ -22,59 +22,58 @@ limitations under the License.
* Fires when the middle panel has been resized by a pixel.
* @event module:utils~ResizeNotifier#"middlePanelResizedNoisy"
*/
+
import { EventEmitter } from "events";
import { throttle } from "lodash";
export default class ResizeNotifier extends EventEmitter {
- constructor() {
- super();
- // with default options, will call fn once at first call, and then every x ms
- // if there was another call in that timespan
- this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
- this._isResizing = false;
- }
+ private _isResizing = false;
- get isResizing() {
+ // with default options, will call fn once at first call, and then every x ms
+ // if there was another call in that timespan
+ private throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
+
+ public get isResizing() {
return this._isResizing;
}
- startResizing() {
+ public startResizing() {
this._isResizing = true;
this.emit("isResizing", true);
}
- stopResizing() {
+ public stopResizing() {
this._isResizing = false;
this.emit("isResizing", false);
}
- _noisyMiddlePanel() {
+ private noisyMiddlePanel() {
this.emit("middlePanelResizedNoisy");
}
- _updateMiddlePanel() {
- this._throttledMiddlePanel();
- this._noisyMiddlePanel();
+ private updateMiddlePanel() {
+ this.throttledMiddlePanel();
+ this.noisyMiddlePanel();
}
// can be called in quick succession
- notifyLeftHandleResized() {
+ public notifyLeftHandleResized() {
// don't emit event for own region
- this._updateMiddlePanel();
+ this.updateMiddlePanel();
}
// can be called in quick succession
- notifyRightHandleResized() {
- this._updateMiddlePanel();
+ public notifyRightHandleResized() {
+ this.updateMiddlePanel();
}
- notifyTimelineHeightChanged() {
- this._updateMiddlePanel();
+ public notifyTimelineHeightChanged() {
+ this.updateMiddlePanel();
}
// can be called in quick succession
- notifyWindowResized() {
- this._updateMiddlePanel();
+ public notifyWindowResized() {
+ this.updateMiddlePanel();
}
}
diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts
index 5fe653fed0..c855b81bf8 100644
--- a/src/utils/ShieldUtils.ts
+++ b/src/utils/ShieldUtils.ts
@@ -1,30 +1,31 @@
+/*
+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 { MatrixClient } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+
import DMRoomMap from './DMRoomMap';
-/* For now, a cut-down type spec for the client */
-interface Client {
- getUserId: () => string;
- checkUserTrust: (userId: string) => {
- isCrossSigningVerified: () => boolean
- wasCrossSigningVerified: () => boolean
- };
- getStoredDevicesForUser: (userId: string) => [{ deviceId: string }];
- checkDeviceTrust: (userId: string, deviceId: string) => {
- isVerified: () => boolean
- };
-}
-
-interface Room {
- getEncryptionTargetMembers: () => Promise<[{userId: string}]>;
- roomId: string;
-}
-
export enum E2EStatus {
Warning = "warning",
Verified = "verified",
Normal = "normal"
}
-export async function shieldStatusForRoom(client: Client, room: Room): Promise {
+export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise {
const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId);
const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index c67f3bad13..926278a20a 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -16,19 +16,20 @@ limitations under the License.
*/
import * as url from "url";
+import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { MatrixClientPeg } from '../MatrixClientPeg';
import SdkConfig from "../SdkConfig";
import dis from '../dispatcher/dispatcher';
import WidgetEchoStore from '../stores/WidgetEchoStore';
import SettingsStore from "../settings/SettingsStore";
-import {IntegrationManagers} from "../integrations/IntegrationManagers";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {WidgetType} from "../widgets/WidgetType";
-import {objectClone} from "./objects";
-import {_t} from "../languageHandler";
-import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api";
-import {IApp} from "../stores/WidgetStore";
+import { IntegrationManagers } from "../integrations/IntegrationManagers";
+import { WidgetType } from "../widgets/WidgetType";
+import { objectClone } from "./objects";
+import { _t } from "../languageHandler";
+import { IApp } from "../stores/WidgetStore";
// How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise
@@ -377,9 +378,9 @@ export default class WidgetUtils {
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
}
- static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] {
- const widgets = WidgetUtils.getRoomWidgets(room);
- return (widgets || []).filter(w => {
+ static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] {
+ const widgets = WidgetUtils.getRoomWidgets(room) || [];
+ return widgets.filter(w => {
const content = w.getContent();
return content.url && type.matches(content.type);
});
@@ -392,7 +393,7 @@ export default class WidgetUtils {
}
const widgets = client.getAccountData('m.widgets');
if (!widgets) return;
- const userWidgets: IWidgetEvent[] = widgets.getContent() || {};
+ const userWidgets: Record = widgets.getContent() || {};
Object.entries(userWidgets).forEach(([key, widget]) => {
if (widget.content && widget.content.type === "m.integration_manager") {
delete userWidgets[key];
diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts
index e527f43c29..6524debfb7 100644
--- a/src/utils/arrays.ts
+++ b/src/utils/arrays.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {percentageOf, percentageWithin} from "./numbers";
+import { percentageOf, percentageWithin } from "./numbers";
/**
* Quickly resample an array to have less/more data points. If an input which is larger
@@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] {
}, new Set()));
}
+/**
+ * Moves a single element from fromIndex to toIndex.
+ * @param {array} list the list from which to construct the new list.
+ * @param {number} fromIndex the index of the element to move.
+ * @param {number} toIndex the index of where to put the element.
+ * @returns {array} A new array with the requested value moved.
+ */
+export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] {
+ const result = Array.from(list);
+ const [removed] = result.splice(fromIndex, 1);
+ result.splice(toIndex, 0, removed);
+
+ return result;
+}
+
/**
* Helper functions to perform LINQ-like queries on arrays.
*/
diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.ts
similarity index 76%
rename from src/utils/createMatrixClient.js
rename to src/utils/createMatrixClient.ts
index f5e196d846..caaf75616d 100644
--- a/src/utils/createMatrixClient.js
+++ b/src/utils/createMatrixClient.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2017 Vector Creations 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,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {createClient} from "matrix-js-sdk/src/matrix";
-import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
-import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage";
-import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb";
+import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
+import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
+import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage";
+import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
const localStorage = window.localStorage;
@@ -41,8 +41,8 @@ try {
*
* @returns {MatrixClient} the newly-created MatrixClient
*/
-export default function createMatrixClient(opts) {
- const storeOpts = {
+export default function createMatrixClient(opts: ICreateClientOpts) {
+ const storeOpts: Partial = {
useAuthorizationHeader: true,
};
@@ -65,9 +65,10 @@ export default function createMatrixClient(opts) {
);
}
- opts = Object.assign(storeOpts, opts);
-
- return createClient(opts);
+ return createClient({
+ ...storeOpts,
+ ...opts,
+ });
}
createMatrixClient.indexedDbWorkerScript = null;
diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts
new file mode 100644
index 0000000000..da840792ee
--- /dev/null
+++ b/src/utils/stringOrderField.ts
@@ -0,0 +1,148 @@
+/*
+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 { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { moveElement } from "./arrays";
+
+export function midPointsBetweenStrings(
+ a: string,
+ b: string,
+ count: number,
+ maxLen: number,
+ alphabet = DEFAULT_ALPHABET,
+): string[] {
+ const padN = Math.min(Math.max(a.length, b.length), maxLen);
+ const padA = alphabetPad(a, padN, alphabet);
+ const padB = alphabetPad(b, padN, alphabet);
+ const baseA = stringToBase(padA, alphabet);
+ const baseB = stringToBase(padB, alphabet);
+
+ if (baseB - baseA - BigInt(1) < count) {
+ if (padN < maxLen) {
+ // this recurses once at most due to the new limit of n+1
+ return midPointsBetweenStrings(
+ alphabetPad(padA, padN + 1, alphabet),
+ alphabetPad(padB, padN + 1, alphabet),
+ count,
+ padN + 1,
+ alphabet,
+ );
+ }
+ return [];
+ }
+
+ const step = (baseB - baseA) / BigInt(count + 1);
+ const start = BigInt(baseA + step);
+ return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet));
+}
+
+interface IEntry {
+ index: number;
+ order: string;
+}
+
+export const reorderLexicographically = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ maxLen = 50,
+): IEntry[] => {
+ // sanity check inputs
+ if (
+ fromIndex < 0 || toIndex < 0 ||
+ fromIndex > orders.length || toIndex > orders.length ||
+ fromIndex === toIndex
+ ) {
+ return [];
+ }
+
+ // zip orders with their indices to simplify later index wrangling
+ const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order }));
+ // apply the fundamental order update to the zipped array
+ const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex);
+
+ // check if we have to fill undefined orders to complete placement
+ const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined;
+
+ let leftBoundIdx = toIndex;
+ let rightBoundIdx = toIndex;
+
+ let canMoveLeft = true;
+ const nextBase = newOrder[toIndex + 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex + 1].order)
+ : BigInt(Number.MAX_VALUE);
+
+ // check how far left we would have to mutate to fit in that direction
+ for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) {
+ if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break;
+ leftBoundIdx = i;
+ }
+
+ // verify the left move would be sufficient
+ const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order);
+ const bigToIndex = BigInt(toIndex);
+ if (leftBoundIdx === 0 &&
+ firstOrderBase !== undefined &&
+ nextBase - firstOrderBase <= bigToIndex &&
+ firstOrderBase <= bigToIndex
+ ) {
+ canMoveLeft = false;
+ }
+
+ const canDisplaceRight = !orderToLeftUndefined;
+ let canMoveRight = canDisplaceRight;
+ if (canDisplaceRight) {
+ const prevBase = newOrder[toIndex - 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex - 1]?.order)
+ : BigInt(Number.MIN_VALUE);
+
+ // check how far right we would have to mutate to fit in that direction
+ for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) {
+ if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break;
+ rightBoundIdx = i;
+ }
+
+ // verify the right move would be sufficient
+ if (rightBoundIdx === newOrder.length - 1 &&
+ (newOrder[rightBoundIdx]
+ ? stringToBase(newOrder[rightBoundIdx].order)
+ : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex)
+ ) {
+ canMoveRight = false;
+ }
+ }
+
+ // pick the cheaper direction
+ const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER;
+ const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER;
+ if (orderToLeftUndefined || leftDiff < rightDiff) {
+ rightBoundIdx = toIndex;
+ } else {
+ leftBoundIdx = toIndex;
+ }
+
+ const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? "";
+ const nextOrder = newOrder[rightBoundIdx + 1]?.order
+ ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1);
+
+ const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen);
+
+ return changes.map((order, i) => ({
+ index: newOrder[leftBoundIdx + i].index,
+ order,
+ }));
+};
diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts
index fde5779fa2..8f9e03bb8e 100644
--- a/src/voice/VoiceRecording.ts
+++ b/src/voice/VoiceRecording.ts
@@ -17,7 +17,7 @@ limitations under the License.
import * as Recorder from 'opus-recorder';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
import {MatrixClient} from "matrix-js-sdk/src/client";
-import CallMediaHandler from "../CallMediaHandler";
+import MediaDeviceHandler from "../MediaDeviceHandler";
import {SimpleObservable} from "matrix-widget-api";
import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
import EventEmitter from "events";
@@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
audio: {
channelCount: CHANNELS,
noiseSuppression: true, // browsers ignore constraints they can't honour
- deviceId: CallMediaHandler.getAudioInput(),
+ deviceId: MediaDeviceHandler.getAudioInput(),
},
});
this.recorderContext = createAudioContext({
diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js
index 7a6a42ef55..bc751ba44e 100644
--- a/test/DecryptionFailureTracker-test.js
+++ b/test/DecryptionFailureTracker-test.js
@@ -30,9 +30,7 @@ function createFailedDecryptionEvent() {
const event = new MatrixEvent({
event_id: "event-id-" + Math.random().toString(16).slice(2),
});
- event._setClearData(
- event._badEncryptedMessage(":("),
- );
+ event.setClearData(event.badEncryptedMessage(":("));
return event;
}
@@ -67,7 +65,7 @@ describe('DecryptionFailureTracker', function() {
tracker.eventDecrypted(decryptedEvent, err);
// Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
- decryptedEvent._setClearData({});
+ decryptedEvent.setClearData({});
tracker.eventDecrypted(decryptedEvent, null);
// Pretend "now" is Infinity
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 8f0242eb30..d32970a278 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
configure({ adapter: new Adapter() });
let client;
-const room = new Matrix.Room();
+const room = new Matrix.Room("!roomId:server_name");
// wrap MessagePanel with a component which provides the MatrixClient in the context.
class WrappedMessagePanel extends React.Component {
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index bfb8e1afd4..6aad6a90fd 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils';
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
import sdk from '../../../skinned-sdk';
-import { DragDropContext } from 'react-beautiful-dnd';
import dis from '../../../../src/dispatcher/dispatcher';
import DMRoomMap from '../../../../src/utils/DMRoomMap';
@@ -68,9 +67,7 @@ describe('RoomList', () => {
const RoomList = sdk.getComponent('views.rooms.RoomList');
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render(
-
- {}} />
- ,
+ {}} />,
parentDiv,
);
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js
index 20e8af2947..b20874fdaf 100644
--- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js
+++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js
@@ -20,9 +20,11 @@ const acceptInvite = require('../usecases/accept-invite');
const {receiveMessage} = require('../usecases/timeline');
const {createDm} = require('../usecases/create-room');
const {checkRoomSettings} = require('../usecases/room-settings');
-const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify');
+const {startSasVerification, acceptSasVerification} = require('../usecases/verify');
const { setupSecureBackup } = require('../usecases/security');
const assert = require('assert');
+const { measureStart, measureStop } = require('../util');
+
module.exports = async function e2eEncryptionScenarios(alice, bob) {
console.log(" creating an e2e encrypted DM and join through invite:");
@@ -31,12 +33,14 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) {
await acceptInvite(alice, 'bob');
// do sas verifcation
bob.log.step(`starts SAS verification with ${alice.username}`);
- const bobSasPromise = startSasVerifcation(bob, alice.username);
+ await measureStart(bob, "mx_VerifyE2EEUser");
+ const bobSasPromise = startSasVerification(bob, alice.username);
const aliceSasPromise = acceptSasVerification(alice, bob.username);
// wait in parallel, so they don't deadlock on each other
// the logs get a bit messy here, but that's fine enough for debugging (hopefully)
const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]);
assert.deepEqual(bobSas, aliceSas);
+ await measureStop(bob, "mx_VerifyE2EEUser");
bob.log.done(`done (match for ${bobSas.join(", ")})`);
const aliceMessage = "Guess what I just heard?!";
await sendMessage(alice, aliceMessage);
diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js
index 3830e3e0da..36b9ed21ec 100644
--- a/test/end-to-end-tests/src/usecases/create-room.js
+++ b/test/end-to-end-tests/src/usecases/create-room.js
@@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+const { measureStart, measureStop } = require('../util');
+
async function openRoomDirectory(session) {
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
await roomDirectoryButton.click();
@@ -52,6 +54,8 @@ async function createRoom(session, roomName, encrypted=false) {
async function createDm(session, invitees) {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
+ await measureStart(session, "mx_CreateDM");
+
const dmsSublist = await findSublist(session, "people");
const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton");
await startChatButton.click();
@@ -76,6 +80,8 @@ async function createDm(session, invitees) {
await session.query('.mx_MessageComposer');
session.log.done();
+
+ await measureStop(session, "mx_CreateDM");
}
module.exports = {openRoomDirectory, findSublist, createRoom, createDm};
diff --git a/test/end-to-end-tests/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js
index 655c0be686..cf0f67be44 100644
--- a/test/end-to-end-tests/src/usecases/join.js
+++ b/test/end-to-end-tests/src/usecases/join.js
@@ -16,9 +16,12 @@ limitations under the License.
*/
const {openRoomDirectory} = require('./create-room');
+const { measureStart, measureStop } = require('../util');
+
module.exports = async function join(session, roomName) {
session.log.step(`joins room "${roomName}"`);
+ await measureStart(session, "mx_JoinRoom");
await openRoomDirectory(session);
const roomInput = await session.query('.mx_DirectorySearchBox input');
await session.replaceInputText(roomInput, roomName);
@@ -26,5 +29,6 @@ module.exports = async function join(session, roomName) {
const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton');
await joinFirstLink.click();
await session.query('.mx_MessageComposer');
+ await measureStop(session, "mx_JoinRoom");
session.log.done();
};
diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js
index abd4488db2..654c461296 100644
--- a/test/end-to-end-tests/src/usecases/room-settings.js
+++ b/test/end-to-end-tests/src/usecases/room-settings.js
@@ -140,8 +140,6 @@ async function changeRoomSettings(session, settings) {
if (settings.alias) {
session.log.step(`sets alias to ${settings.alias}`);
- const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
- await summary.click();
const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js
index ea5b9961a4..a66c8c1b1c 100644
--- a/test/end-to-end-tests/src/usecases/verify.js
+++ b/test/end-to-end-tests/src/usecases/verify.js
@@ -74,7 +74,7 @@ async function doSasVerification(session) {
return sasCodes;
}
-module.exports.startSasVerifcation = async function(session, name) {
+module.exports.startSasVerification = async function(session, name) {
session.log.startGroup("starts verification");
await startVerification(session, name);
diff --git a/test/end-to-end-tests/src/util.js b/test/end-to-end-tests/src/util.js
index cc7391fa9f..5abb110df4 100644
--- a/test/end-to-end-tests/src/util.js
+++ b/test/end-to-end-tests/src/util.js
@@ -26,3 +26,15 @@ module.exports.range = function(start, amount, step = 1) {
module.exports.delay = function(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
};
+
+module.exports.measureStart = function(session, name) {
+ return session.page.evaluate(_name => {
+ window.mxPerformanceMonitor.start(_name);
+ }, name);
+};
+
+module.exports.measureStop = function(session, name) {
+ return session.page.evaluate(_name => {
+ window.mxPerformanceMonitor.stop(_name);
+ }, name);
+};
diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js
index c1588e848e..04df0c51c0 100644
--- a/test/end-to-end-tests/start.js
+++ b/test/end-to-end-tests/start.js
@@ -88,6 +88,10 @@ async function runTests() {
window.mxPerformanceMonitor.addPerformanceDataCallback({
entryNames: [
window.mxPerformanceEntryNames.REGISTER,
+ window.mxPerformanceEntryNames.LOGIN,
+ window.mxPerformanceEntryNames.JOIN_ROOM,
+ window.mxPerformanceEntryNames.CREATE_DM,
+ window.mxPerformanceEntryNames.VERIFY_E2EE_USER,
],
callback: (events) => {
measurements = JSON.stringify(events);
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
index 01bd528b87..4cbd9f43c8 100644
--- a/test/stores/SpaceStore-test.ts
+++ b/test/stores/SpaceStore-test.ts
@@ -123,8 +123,15 @@ describe("SpaceStore", () => {
jest.runAllTimers();
client.getVisibleRooms.mockReturnValue(rooms = []);
getValue.mockImplementation(settingName => {
- if (settingName === "feature_spaces") {
- return true;
+ switch (settingName) {
+ case "feature_spaces":
+ return true;
+ case "feature_spaces.all_rooms":
+ return true;
+ case "feature_spaces.space_member_dms":
+ return true;
+ case "feature_spaces.space_dm_badges":
+ return false;
}
});
});
diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts
new file mode 100644
index 0000000000..331627dfc0
--- /dev/null
+++ b/test/utils/stringOrderField-test.ts
@@ -0,0 +1,291 @@
+/*
+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 { sortBy } from "lodash";
+import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField";
+
+const moveLexicographicallyTest = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ expectedChanges: number,
+ maxLength?: number,
+): void => {
+ const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
+
+ const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
+ ops.forEach(({ index, order }) => {
+ zipped[index][1] = order;
+ });
+
+ const newOrders = sortBy(zipped, i => i[1]);
+ expect(newOrders[toIndex][0]).toBe(fromIndex);
+ expect(ops).toHaveLength(expectedChanges);
+};
+
+describe("stringOrderField", () => {
+ describe("midPointsBetweenStrings", () => {
+ it("should work", () => {
+ expect(averageBetweenStrings("!!", "##")).toBe('""');
+ const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
+ expect(midpoints[0]).toBe("a");
+ expect(midpoints[4]).toBe("e");
+ expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
+ });
+
+ it("should return empty array when the request is not possible", () => {
+ expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
+ expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
+ });
+ });
+
+ describe("reorderLexicographically", () => {
+ it("should work when moving left", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
+ });
+
+ it("should work when moving right", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
+ });
+
+ it("should work when all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work when moving to end and all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work when moving left and some orders are undefined", () => {
+ moveLexicographicallyTest(
+ ["a", "c", "e", undefined, undefined, undefined],
+ 5,
+ 2,
+ 1,
+ );
+
+ moveLexicographicallyTest(
+ ["a", "a", "e", undefined, undefined, undefined],
+ 5,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving to the start when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 2,
+ 0,
+ 1,
+ );
+ });
+
+ it("should work moving to the end when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 3,
+ 4,
+ );
+ });
+
+ it("should work moving left when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 2,
+ 3,
+ );
+ });
+
+ it("should work moving more right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work moving left when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["20", undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 2,
+ 2,
+ );
+ });
+
+ it("should work moving right when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 4,
+ );
+ });
+
+ it("should work moving left when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", undefined, undefined],
+ 3,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", "50", undefined],
+ 1,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 2,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 1,
+ 2,
+ 1,
+ );
+ });
+
+ it("should work moving left into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "19"],
+ 3,
+ 1,
+ 2,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(0),
+ // Target
+ DEFAULT_ALPHABET.charAt(1),
+ DEFAULT_ALPHABET.charAt(2),
+ DEFAULT_ALPHABET.charAt(3),
+ DEFAULT_ALPHABET.charAt(4),
+ DEFAULT_ALPHABET.charAt(5),
+ ],
+ 5,
+ 1,
+ 5,
+ 1,
+ );
+ });
+
+ it("should work moving right into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 1,
+ 3,
+ 3,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving right into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "15", "16", undefined],
+ 1,
+ 3,
+ 3,
+ );
+
+ moveLexicographicallyTest(
+ ["0", "1", "2", "3", "4", "5"],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 4,
+ 3,
+ 4,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 4,
+ 3,
+ 4,
+ 1,
+ );
+ });
+ });
+});
+
diff --git a/yarn.lock b/yarn.lock
index cd4a8b0bd6..3bcb8de404 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1017,13 +1017,20 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
+ version "7.14.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
+ integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
@@ -1327,6 +1334,7 @@
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
version "3.2.3"
+ uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
@@ -1479,6 +1487,11 @@
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
+"@types/diff-match-patch@^1.0.32":
+ version "1.0.32"
+ resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f"
+ integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==
+
"@types/events@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -1504,6 +1517,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -1620,12 +1641,29 @@
dependencies:
"@types/node" "*"
-"@types/react-dom@^16.9.10":
- version "16.9.10"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f"
- integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==
+"@types/react-beautiful-dnd@^13.0.0":
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
+ integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
dependencies:
- "@types/react" "^16"
+ "@types/react" "*"
+
+"@types/react-dom@^17.0.2":
+ version "17.0.8"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc"
+ integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react-redux@^7.1.16":
+ version "7.1.16"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
+ integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
"@types/react-transition-group@^4.4.0":
version "4.4.0"
@@ -1634,12 +1672,13 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9":
- version "16.14.2"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c"
- integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==
+"@types/react@*", "@types/react@^17.0.2":
+ version "17.0.11"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
+ integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==
dependencies:
"@types/prop-types" "*"
+ "@types/scheduler" "*"
csstype "^3.0.2"
"@types/sanitize-html@^2.3.1":
@@ -1649,6 +1688,11 @@
dependencies:
htmlparser2 "^6.0.0"
+"@types/scheduler@*":
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
+ integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -2116,14 +2160,6 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
-babel-runtime@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
- integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
- dependencies:
- core-js "^2.4.0"
- regenerator-runtime "^0.11.0"
-
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -2642,11 +2678,6 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
-core-js@^2.4.0:
- version "2.6.12"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
- integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
-
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2706,6 +2737,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
+css-box-model@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
+ integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+ dependencies:
+ tiny-invariant "^1.0.6"
+
css-select@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
@@ -4235,7 +4273,7 @@ highlight.js@^10.5.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
-hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -4450,13 +4488,6 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
- integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
- dependencies:
- loose-envify "^1.0.0"
-
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -5594,11 +5625,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
-lodash-es@^4.2.1:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
- integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
-
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
@@ -5619,7 +5645,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5711,9 +5737,10 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
-"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "11.2.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
+matrix-js-sdk@12.0.0:
+ version "12.0.0"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd"
+ integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ==
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"
@@ -5746,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17"
walk "^2.3.14"
-matrix-widget-api@^0.1.0-beta.14:
- version "0.1.0-beta.14"
- resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
- integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
+matrix-widget-api@^0.1.0-beta.15:
+ version "0.1.0-beta.15"
+ resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
+ integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -5787,10 +5814,10 @@ mdurl@~1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
-memoize-one@^3.0.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
- integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
+memoize-one@^5.1.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
+ integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
meow@^9.0.0:
version "9.0.0"
@@ -6436,11 +6463,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
- integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
-
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6650,7 +6672,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
+prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -6727,12 +6749,12 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-raf-schd@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62"
- integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g==
+raf-schd@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
+ integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
-raf@^3.1.0, raf@^3.4.1:
+raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@@ -6759,21 +6781,18 @@ re-resizable@^6.9.0:
dependencies:
fast-memoize "^2.5.1"
-react-beautiful-dnd@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
- integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==
+react-beautiful-dnd@^13.1.0:
+ version "13.1.0"
+ resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
+ integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
dependencies:
- babel-runtime "^6.26.0"
- invariant "^2.2.2"
- memoize-one "^3.0.1"
- prop-types "^15.6.0"
- raf-schd "^2.1.0"
- react-motion "^0.5.2"
- react-redux "^5.0.6"
- redux "^3.7.2"
- redux-thunk "^2.2.0"
- reselect "^3.0.1"
+ "@babel/runtime" "^7.9.2"
+ css-box-model "^1.2.0"
+ memoize-one "^5.1.1"
+ raf-schd "^4.0.2"
+ react-redux "^7.2.0"
+ redux "^4.0.4"
+ use-memo-one "^1.1.1"
react-clientside-effect@^1.2.2:
version "1.2.3"
@@ -6808,7 +6827,7 @@ react-focus-lock@^2.5.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6818,32 +6837,17 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
-react-lifecycles-compat@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
- integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
-
-react-motion@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
- integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
+react-redux@^7.2.0:
+ version "7.2.4"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+ integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
dependencies:
- performance-now "^0.2.0"
- prop-types "^15.5.8"
- raf "^3.1.0"
-
-react-redux@^5.0.6:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57"
- integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==
- dependencies:
- "@babel/runtime" "^7.1.2"
- hoist-non-react-statics "^3.3.0"
- invariant "^2.2.4"
- loose-envify "^1.1.0"
- prop-types "^15.6.1"
- react-is "^16.6.0"
- react-lifecycles-compat "^3.0.0"
+ "@babel/runtime" "^7.12.1"
+ "@types/react-redux" "^7.1.16"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
react-shallow-renderer@^16.13.1:
version "16.14.1"
@@ -6972,20 +6976,12 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
-redux-thunk@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
- integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
-
-redux@^3.7.2:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
- integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
+redux@^4.0.0, redux@^4.0.4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
+ integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
dependencies:
- lodash "^4.2.1"
- lodash-es "^4.2.1"
- loose-envify "^1.1.0"
- symbol-observable "^1.0.3"
+ "@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
@@ -6999,11 +6995,6 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-regenerator-runtime@^0.11.0:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
- integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
@@ -7161,11 +7152,6 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-reselect@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
- integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
-
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -7888,11 +7874,6 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
-symbol-observable@^1.0.3:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
- integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
-
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -7955,6 +7936,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+tiny-invariant@^1.0.6:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
+ integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+
tmatch@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
@@ -8270,6 +8256,11 @@ use-callback-ref@^1.2.1:
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
+use-memo-one@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+ integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
+
use-sidecar@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"