Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/12740
Conflicts: src/components/views/messages/TextualBody.js src/components/views/right_panel/UserInfo.tsx src/dispatcher/actions.ts
This commit is contained in:
commit
974d62e347
46 changed files with 957 additions and 649 deletions
18
.eslintrc.js
18
.eslintrc.js
|
@ -30,6 +30,24 @@ module.exports = {
|
||||||
|
|
||||||
"quotes": "off",
|
"quotes": "off",
|
||||||
"no-extra-boolean-cast": "off",
|
"no-extra-boolean-cast": "off",
|
||||||
|
"no-restricted-properties": [
|
||||||
|
"error",
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
|
"Use UIStore to access window dimensions instead",
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buildRestrictedPropertiesOptions(properties, message) {
|
||||||
|
return properties.map(prop => {
|
||||||
|
const [object, property] = prop.split(".");
|
||||||
|
return {
|
||||||
|
object,
|
||||||
|
property,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,8 @@ html {
|
||||||
N.B. Breaks things when we have legitimate horizontal overscroll */
|
N.B. Breaks things when we have legitimate horizontal overscroll */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
// Stop similar overscroll bounce in Firefox Nightly for macOS
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -61,8 +61,7 @@ limitations under the License.
|
||||||
&.mx_RoomSublist_headerContainer_sticky {
|
&.mx_RoomSublist_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
// width set by JS
|
width: calc(100% - 15px);
|
||||||
width: calc(100% - 22px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't have a top style because the top is dependent on the room list header's
|
// We don't have a top style because the top is dependent on the room list header's
|
||||||
|
|
|
@ -22,6 +22,7 @@ import SdkConfig from './SdkConfig';
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import {sleep} from "./utils/promise";
|
import {sleep} from "./utils/promise";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
|
import { Action } from "./dispatcher/actions";
|
||||||
|
|
||||||
// polyfill textencoder if necessary
|
// polyfill textencoder if necessary
|
||||||
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
|
||||||
|
@ -265,7 +266,7 @@ interface ICreateRoomEvent extends IEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IJoinRoomEvent extends IEvent {
|
interface IJoinRoomEvent extends IEvent {
|
||||||
key: "join_room";
|
key: Action.JoinRoom;
|
||||||
dur: number; // how long it took to join (until remote echo)
|
dur: number; // how long it took to join (until remote echo)
|
||||||
segmentation: {
|
segmentation: {
|
||||||
room_id: string; // hashed
|
room_id: string; // hashed
|
||||||
|
@ -684,7 +685,9 @@ export default class CountlyAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOrientation = (): Orientation => {
|
private getOrientation = (): Orientation => {
|
||||||
return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait;
|
return window.matchMedia("(orientation: landscape)").matches
|
||||||
|
? Orientation.Landscape
|
||||||
|
: Orientation.Portrait
|
||||||
};
|
};
|
||||||
|
|
||||||
private reportOrientation = () => {
|
private reportOrientation = () => {
|
||||||
|
@ -858,7 +861,7 @@ export default class CountlyAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
|
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
|
||||||
this.track<IJoinRoomEvent>("join_room", { type }, roomId, {
|
this.track<IJoinRoomEvent>(Action.JoinRoom, { type }, roomId, {
|
||||||
dur: CountlyAnalytics.getTimestamp() - startTime,
|
dur: CountlyAnalytics.getTimestamp() - startTime,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import MultiInviter from './utils/MultiInviter';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import GroupStore from './stores/GroupStore';
|
import GroupStore from './stores/GroupStore';
|
||||||
import {allSettled} from "./utils/promise";
|
|
||||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||||
|
|
||||||
export function showGroupInviteDialog(groupId) {
|
export function showGroupInviteDialog(groupId) {
|
||||||
|
@ -120,7 +119,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||||
const matrixClient = MatrixClientPeg.get();
|
const matrixClient = MatrixClientPeg.get();
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
return allSettled(addrs.map((addr) => {
|
return Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
|
||||||
.catch(() => { errorList.push(addr.address); })
|
.catch(() => { errorList.push(addr.address); })
|
||||||
|
|
14
src/Terms.ts
14
src/Terms.ts
|
@ -36,14 +36,18 @@ export class Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Policy {
|
export interface LocalisedPolicy {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Policy {
|
||||||
// @ts-ignore: No great way to express indexed types together with other keys
|
// @ts-ignore: No great way to express indexed types together with other keys
|
||||||
version: string;
|
version: string;
|
||||||
[lang: string]: {
|
[lang: string]: LocalisedPolicy;
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
type Policies = {
|
|
||||||
|
export type Policies = {
|
||||||
[policy: string]: Policy,
|
[policy: string]: Policy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import classNames from "classnames";
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import {Writeable} from "../../@types/common";
|
import {Writeable} from "../../@types/common";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
const buttonTop = elementRect.top + window.pageYOffset;
|
const buttonTop = elementRect.top + window.pageYOffset;
|
||||||
// Align the right edge of the menu to the right edge of the button
|
// Align the right edge of the menu to the right edge of the button
|
||||||
menuOptions.right = window.innerWidth - buttonRight;
|
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||||
// Align the menu vertically on whichever side of the button has more space available.
|
// Align the menu vertically on whichever side of the button has more space available.
|
||||||
if (buttonBottom < window.innerHeight / 2) {
|
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||||
menuOptions.top = buttonBottom + vPadding;
|
menuOptions.top = buttonBottom + vPadding;
|
||||||
} else {
|
} else {
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
|
@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
const buttonTop = elementRect.top + window.pageYOffset;
|
const buttonTop = elementRect.top + window.pageYOffset;
|
||||||
// Align the right edge of the menu to the right edge of the button
|
// Align the right edge of the menu to the right edge of the button
|
||||||
menuOptions.right = window.innerWidth - buttonRight;
|
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
|
||||||
// Align the menu vertically on whichever side of the button has more space available.
|
// Align the menu vertically on whichever side of the button has more space available.
|
||||||
if (buttonBottom < window.innerHeight / 2) {
|
if (buttonBottom < UIStore.instance.windowHeight / 2) {
|
||||||
menuOptions.top = buttonBottom + vPadding;
|
menuOptions.top = buttonBottom + vPadding;
|
||||||
} else {
|
} else {
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
}
|
}
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
|
@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
|
||||||
// Align the left edge of the menu to the left edge of the button
|
// Align the left edge of the menu to the left edge of the button
|
||||||
menuOptions.left = buttonLeft;
|
menuOptions.left = buttonLeft;
|
||||||
// Align the menu vertically above the menu
|
// Align the menu vertically above the menu
|
||||||
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
|
menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
|
||||||
|
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,7 +36,7 @@ import FlairStore from '../../stores/FlairStore';
|
||||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||||
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
|
||||||
import {Group} from "matrix-js-sdk/src/models/group";
|
import {Group} from "matrix-js-sdk/src/models/group";
|
||||||
import {allSettled, sleep} from "../../utils/promise";
|
import {sleep} from "../../utils/promise";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
|
@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component {
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
allSettled(addrs.map((addr) => {
|
Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
.addRoomToGroupSummary(this.props.groupId, addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); });
|
.catch(() => { errorList.push(addr.address); });
|
||||||
|
@ -274,7 +274,7 @@ class RoleUserList extends React.Component {
|
||||||
onFinished: (success, addrs) => {
|
onFinished: (success, addrs) => {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
allSettled(addrs.map((addr) => {
|
Promise.allSettled(addrs.map((addr) => {
|
||||||
return GroupStore
|
return GroupStore
|
||||||
.addUserToGroupSummary(addr.address)
|
.addUserToGroupSummary(addr.address)
|
||||||
.catch(() => { errorList.push(addr.address); });
|
.catch(() => { errorList.push(addr.address); });
|
||||||
|
|
|
@ -43,6 +43,7 @@ import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import {mediaFromMxc} from "../../customisations/Media";
|
||||||
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -90,10 +91,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||||
});
|
});
|
||||||
|
|
||||||
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
|
||||||
// We listen to the noisy channel to avoid choppy reaction times.
|
|
||||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -103,7 +100,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateActiveSpace = (activeSpace: Room) => {
|
private updateActiveSpace = (activeSpace: Room) => {
|
||||||
|
@ -114,6 +110,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
dis.fire(Action.ViewRoomDirectory);
|
dis.fire(Action.ViewRoomDirectory);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private refreshStickyHeaders = () => {
|
||||||
|
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||||
|
this.handleStickyHeaders(this.listContainerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
private onBreadcrumbsUpdate = () => {
|
private onBreadcrumbsUpdate = () => {
|
||||||
const newVal = BreadcrumbsStore.instance.visible;
|
const newVal = BreadcrumbsStore.instance.visible;
|
||||||
if (newVal !== this.state.showBreadcrumbs) {
|
if (newVal !== this.state.showBreadcrumbs) {
|
||||||
|
@ -156,9 +157,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist:not(.mx_RoomSublist_hidden)");
|
||||||
|
|
||||||
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
|
||||||
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
|
||||||
|
|
||||||
// We track which styles we want on a target before making the changes to avoid
|
// We track which styles we want on a target before making the changes to avoid
|
||||||
// excessive layout updates.
|
// excessive layout updates.
|
||||||
const targetStyles = new Map<HTMLDivElement, {
|
const targetStyles = new Map<HTMLDivElement, {
|
||||||
|
@ -228,7 +226,8 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
|
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
const offset = UIStore.instance.windowHeight -
|
||||||
|
(list.parentElement.offsetTop + list.parentElement.offsetHeight);
|
||||||
const newBottom = `${offset}px`;
|
const newBottom = `${offset}px`;
|
||||||
if (header.style.bottom !== newBottom) {
|
if (header.style.bottom !== newBottom) {
|
||||||
header.style.bottom = newBottom;
|
header.style.bottom = newBottom;
|
||||||
|
@ -246,18 +245,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWidth = `${headerStickyWidth}px`;
|
|
||||||
if (header.style.width !== newWidth) {
|
|
||||||
header.style.width = newWidth;
|
|
||||||
}
|
|
||||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
if (header.style.width) {
|
|
||||||
header.style.removeProperty('width');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,11 +272,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
this.handleStickyHeaders(list);
|
this.handleStickyHeaders(list);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onResize = () => {
|
|
||||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
|
||||||
this.handleStickyHeaders(this.listContainerRef.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onFocus = (ev: React.FocusEvent) => {
|
private onFocus = (ev: React.FocusEvent) => {
|
||||||
this.focusedElement = ev.target;
|
this.focusedElement = ev.target;
|
||||||
};
|
};
|
||||||
|
@ -420,7 +406,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.onResize}
|
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
@ -441,7 +426,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
{this.renderBreadcrumbs()}
|
{this.renderBreadcrumbs()}
|
||||||
<RoomListNumResults />
|
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||||
<div className="mx_LeftPanel_roomListWrapper">
|
<div className="mx_LeftPanel_roomListWrapper">
|
||||||
<div
|
<div
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
|
@ -454,7 +439,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
{roomList}
|
{roomList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
|
{ !this.props.isMinimized && <LeftPanelWidget /> }
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useContext, useEffect, useMemo} from "react";
|
import React, {useContext, useMemo} from "react";
|
||||||
import {Resizable} from "re-resizable";
|
import {Resizable} from "re-resizable";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
@ -27,16 +27,13 @@ import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
|
||||||
import {useAccountData} from "../../hooks/useAccountData";
|
import {useAccountData} from "../../hooks/useAccountData";
|
||||||
import AppTile from "../views/elements/AppTile";
|
import AppTile from "../views/elements/AppTile";
|
||||||
import {useSettingValue} from "../../hooks/useSettings";
|
import {useSettingValue} from "../../hooks/useSettings";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
interface IProps {
|
|
||||||
onResize(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_HEIGHT = 100;
|
const MIN_HEIGHT = 100;
|
||||||
const MAX_HEIGHT = 500; // or 50% of the window height
|
const MAX_HEIGHT = 500; // or 50% of the window height
|
||||||
const INITIAL_HEIGHT = 280;
|
const INITIAL_HEIGHT = 280;
|
||||||
|
|
||||||
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
const LeftPanelWidget: React.FC = () => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
|
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
|
||||||
|
@ -56,7 +53,6 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||||
|
|
||||||
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||||
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||||
useEffect(onResize, [expanded, onResize]);
|
|
||||||
|
|
||||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
@ -68,8 +64,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||||
content = <Resizable
|
content = <Resizable
|
||||||
size={{height} as any}
|
size={{height} as any}
|
||||||
minHeight={MIN_HEIGHT}
|
minHeight={MIN_HEIGHT}
|
||||||
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
|
maxHeight={Math.min(UIStore.instance.windowHeight / 2, MAX_HEIGHT)}
|
||||||
onResize={onResize}
|
|
||||||
onResizeStop={(e, dir, ref, d) => {
|
onResizeStop={(e, dir, ref, d) => {
|
||||||
setHeight(height + d.height);
|
setHeight(height + d.height);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -87,6 +87,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import SecurityCustomisations from "../../customisations/Security";
|
import SecurityCustomisations from "../../customisations/Security";
|
||||||
|
|
||||||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
|
||||||
|
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export enum Views {
|
export enum Views {
|
||||||
|
@ -225,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
firstSyncPromise: IDeferred<void>;
|
firstSyncPromise: IDeferred<void>;
|
||||||
|
|
||||||
private screenAfterLogin?: IScreen;
|
private screenAfterLogin?: IScreen;
|
||||||
private windowWidth: number;
|
|
||||||
private pageChanging: boolean;
|
private pageChanging: boolean;
|
||||||
private tokenLogin?: boolean;
|
private tokenLogin?: boolean;
|
||||||
private accountPassword?: string;
|
private accountPassword?: string;
|
||||||
|
@ -277,9 +277,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.windowWidth = 10000;
|
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
||||||
this.handleResize();
|
|
||||||
window.addEventListener('resize', this.handleResize);
|
|
||||||
|
|
||||||
this.pageChanging = false;
|
this.pageChanging = false;
|
||||||
|
|
||||||
|
@ -436,7 +434,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.themeWatcher.stop();
|
this.themeWatcher.stop();
|
||||||
this.fontWatcher.stop();
|
this.fontWatcher.stop();
|
||||||
window.removeEventListener('resize', this.handleResize);
|
UIStore.destroy();
|
||||||
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
|
||||||
|
|
||||||
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
|
||||||
|
@ -1820,18 +1818,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = () => {
|
||||||
const hideLhsThreshold = 1000;
|
const LHS_THRESHOLD = 1000;
|
||||||
const showLhsThreshold = 1000;
|
const width = UIStore.instance.windowWidth;
|
||||||
|
|
||||||
if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
if (width <= LHS_THRESHOLD && !this.state.collapseLhs) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
}
|
}
|
||||||
if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
if (width > LHS_THRESHOLD && this.state.collapseLhs) {
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.resizeNotifier.notifyWindowResized();
|
this.state.resizeNotifier.notifyWindowResized();
|
||||||
this.windowWidth = window.innerWidth;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private dispatchTimelineResize() {
|
private dispatchTimelineResize() {
|
||||||
|
|
|
@ -83,6 +83,7 @@ import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -1114,7 +1115,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const signUrl = this.props.threepidInvite?.signUrl;
|
const signUrl = this.props.threepidInvite?.signUrl;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'join_room',
|
action: Action.JoinRoom,
|
||||||
|
roomId: this.getRoomId(),
|
||||||
opts: { inviteSignUrl: signUrl },
|
opts: { inviteSignUrl: signUrl },
|
||||||
_type: "unknown", // TODO: instrumentation
|
_type: "unknown", // TODO: instrumentation
|
||||||
});
|
});
|
||||||
|
@ -1585,7 +1587,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
// a maxHeight on the underlying remote video tag.
|
// a maxHeight on the underlying remote video tag.
|
||||||
|
|
||||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||||
let auxPanelMaxHeight = window.innerHeight -
|
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||||
(54 + // height of RoomHeader
|
(54 + // height of RoomHeader
|
||||||
36 + // height of the status area
|
36 + // height of the status area
|
||||||
51 + // minimum height of the message compmoser
|
51 + // minimum height of the message compmoser
|
||||||
|
|
|
@ -101,15 +101,13 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
numChildRooms,
|
numChildRooms,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const name = room.name || room.canonical_alias || room.aliases?.[0]
|
const cli = MatrixClientPeg.get();
|
||||||
|
const joinedRoom = cli.getRoom(room.room_id)?.getMyMembership() === "join" ? cli.getRoom(room.room_id) : null;
|
||||||
|
const name = joinedRoom?.name || room.name || room.canonical_alias || room.aliases?.[0]
|
||||||
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
|| (room.room_type === RoomType.Space ? _t("Unnamed Space") : _t("Unnamed Room"));
|
||||||
|
|
||||||
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
const [showChildren, toggleShowChildren] = useStateToggle(true);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
const cliRoom = cli.getRoom(room.room_id);
|
|
||||||
const myMembership = cliRoom?.getMyMembership();
|
|
||||||
|
|
||||||
const onPreviewClick = (ev: ButtonEvent) => {
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -122,7 +120,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (myMembership === "join") {
|
if (joinedRoom) {
|
||||||
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
|
||||||
{ _t("View") }
|
{ _t("View") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
|
@ -146,17 +144,27 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let url: string;
|
let avatar;
|
||||||
if (room.avatar_url) {
|
if (joinedRoom) {
|
||||||
url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20);
|
avatar = <RoomAvatar room={joinedRoom} width={20} height={20} />;
|
||||||
|
} else {
|
||||||
|
avatar = <BaseAvatar
|
||||||
|
name={name}
|
||||||
|
idName={room.room_id}
|
||||||
|
url={room.avatar_url ? mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20) : null}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let description = _t("%(count)s members", { count: room.num_joined_members });
|
let description = _t("%(count)s members", { count: room.num_joined_members });
|
||||||
if (numChildRooms !== undefined) {
|
if (numChildRooms !== undefined) {
|
||||||
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
description += " · " + _t("%(count)s rooms", { count: numChildRooms });
|
||||||
}
|
}
|
||||||
if (room.topic) {
|
|
||||||
description += " · " + room.topic;
|
const topic = joinedRoom?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || room.topic;
|
||||||
|
if (topic) {
|
||||||
|
description += " · " + topic;
|
||||||
}
|
}
|
||||||
|
|
||||||
let suggestedSection;
|
let suggestedSection;
|
||||||
|
@ -167,7 +175,7 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = <React.Fragment>
|
const content = <React.Fragment>
|
||||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={20} height={20} />
|
{ avatar }
|
||||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||||
{ name }
|
{ name }
|
||||||
{ suggestedSection }
|
{ suggestedSection }
|
||||||
|
|
|
@ -57,7 +57,8 @@ import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
|
||||||
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
import TooltipButton from "../views/elements/TooltipButton";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +69,7 @@ interface IState {
|
||||||
contextMenuPosition: PartialDOMRect;
|
contextMenuPosition: PartialDOMRect;
|
||||||
isDarkTheme: boolean;
|
isDarkTheme: boolean;
|
||||||
selectedSpace?: Room;
|
selectedSpace?: Room;
|
||||||
|
pendingRoomJoin: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.UserMenu")
|
@replaceableComponent("structures.UserMenu")
|
||||||
|
@ -84,6 +86,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
isDarkTheme: this.isUserOnDarkTheme(),
|
isDarkTheme: this.isUserOnDarkTheme(),
|
||||||
|
pendingRoomJoin: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||||
|
@ -147,15 +150,48 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAction = (ev: ActionPayload) => {
|
private onAction = (ev: ActionPayload) => {
|
||||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
switch (ev.action) {
|
||||||
|
case Action.ToggleUserMenu:
|
||||||
if (this.state.contextMenuPosition) {
|
if (this.state.contextMenuPosition) {
|
||||||
this.setState({contextMenuPosition: null});
|
this.setState({contextMenuPosition: null});
|
||||||
} else {
|
} else {
|
||||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Action.JoinRoom:
|
||||||
|
this.addPendingJoinRoom(ev.roomId);
|
||||||
|
break;
|
||||||
|
case Action.JoinRoomReady:
|
||||||
|
case Action.JoinRoomError:
|
||||||
|
this.removePendingJoinRoom(ev.roomId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private addPendingJoinRoom(roomId) {
|
||||||
|
this.setState({
|
||||||
|
pendingRoomJoin: [
|
||||||
|
...this.state.pendingRoomJoin,
|
||||||
|
roomId,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removePendingJoinRoom(roomId) {
|
||||||
|
const newPendingRoomJoin = this.state.pendingRoomJoin.filter(pendingJoinRoomId => {
|
||||||
|
return pendingJoinRoomId !== roomId;
|
||||||
|
});
|
||||||
|
if (newPendingRoomJoin.length !== this.state.pendingRoomJoin.length) {
|
||||||
|
this.setState({
|
||||||
|
pendingRoomJoin: newPendingRoomJoin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPendingActions(): boolean {
|
||||||
|
return this.state.pendingRoomJoin.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -617,6 +653,14 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
{name}
|
{name}
|
||||||
|
{this.hasPendingActions && (
|
||||||
|
<InlineSpinner>
|
||||||
|
<TooltipButton helpText={_t(
|
||||||
|
"Currently joining %(count)s rooms",
|
||||||
|
{ count: this.state.pendingRoomJoin.length },
|
||||||
|
)} />
|
||||||
|
</InlineSpinner>
|
||||||
|
)}
|
||||||
{dnd}
|
{dnd}
|
||||||
{buttons}
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import classnames from 'classnames';
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import { LocalisedPolicy, Policies } from '../../../Terms';
|
||||||
|
|
||||||
/* This file contains a collection of components which are used by the
|
/* This file contains a collection of components which are used by the
|
||||||
* InteractiveAuth to prompt the user to enter the information needed
|
* InteractiveAuth to prompt the user to enter the information needed
|
||||||
|
@ -74,36 +73,72 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
* focus: set the input focus appropriately in the form.
|
* focus: set the input focus appropriately in the form.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
enum AuthType {
|
||||||
|
Password = "m.login.password",
|
||||||
|
Recaptcha = "m.login.recaptcha",
|
||||||
|
Terms = "m.login.terms",
|
||||||
|
Email = "m.login.email.identity",
|
||||||
|
Msisdn = "m.login.msisdn",
|
||||||
|
Sso = "m.login.sso",
|
||||||
|
SsoUnstable = "org.matrix.login.sso",
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
interface IAuthDict {
|
||||||
|
type?: AuthType;
|
||||||
|
// TODO: Remove `user` once servers support proper UIA
|
||||||
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
|
user?: string;
|
||||||
|
identifier?: any;
|
||||||
|
password?: string;
|
||||||
|
response?: string;
|
||||||
|
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||||
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
|
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||||
|
threepid_creds?: any;
|
||||||
|
threepidCreds?: any;
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
export const DEFAULT_PHASE = 0;
|
export const DEFAULT_PHASE = 0;
|
||||||
|
|
||||||
@replaceableComponent("views.auth.PasswordAuthEntry")
|
interface IAuthEntryProps {
|
||||||
export class PasswordAuthEntry extends React.Component {
|
matrixClient: MatrixClient;
|
||||||
static LOGIN_TYPE = "m.login.password";
|
loginType: string;
|
||||||
|
authSessionId: string;
|
||||||
|
errorText?: string;
|
||||||
|
// Is the auth logic currently waiting for something to happen?
|
||||||
|
busy?: boolean;
|
||||||
|
onPhaseChange: (phase: number) => void;
|
||||||
|
submitAuthDict: (auth: IAuthDict) => void;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
interface IPasswordAuthEntryState {
|
||||||
matrixClient: PropTypes.object.isRequired,
|
password: string;
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
}
|
||||||
errorText: PropTypes.string,
|
|
||||||
// is the auth logic currently waiting for something to
|
@replaceableComponent("views.auth.PasswordAuthEntry")
|
||||||
// happen?
|
export class PasswordAuthEntry extends React.Component<IAuthEntryProps, IPasswordAuthEntryState> {
|
||||||
busy: PropTypes.bool,
|
static LOGIN_TYPE = AuthType.Password;
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
private onSubmit = (e: FormEvent) => {
|
||||||
password: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
_onSubmit = e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.props.busy) return;
|
if (this.props.busy) return;
|
||||||
|
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
type: AuthType.Password,
|
||||||
// TODO: Remove `user` once servers support proper UIA
|
// TODO: Remove `user` once servers support proper UIA
|
||||||
// See https://github.com/vector-im/element-web/issues/10312
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
user: this.props.matrixClient.credentials.userId,
|
user: this.props.matrixClient.credentials.userId,
|
||||||
|
@ -115,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPasswordFieldChange = ev => {
|
private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
// enable the submit button iff the password is non-empty
|
// enable the submit button iff the password is non-empty
|
||||||
this.setState({
|
this.setState({
|
||||||
password: ev.target.value,
|
password: ev.target.value,
|
||||||
|
@ -123,7 +158,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const passwordBoxClass = classnames({
|
const passwordBoxClass = classNames({
|
||||||
"error": this.props.errorText,
|
"error": this.props.errorText,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -155,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
<form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||||
<Field
|
<Field
|
||||||
className={passwordBoxClass}
|
className={passwordBoxClass}
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -163,7 +198,7 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
label={_t('Password')}
|
label={_t('Password')}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this._onPasswordFieldChange}
|
onChange={this.onPasswordFieldChange}
|
||||||
/>
|
/>
|
||||||
<div className="mx_button_row">
|
<div className="mx_button_row">
|
||||||
{ submitButtonOrSpinner }
|
{ submitButtonOrSpinner }
|
||||||
|
@ -175,26 +210,26 @@ export class PasswordAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
/* eslint-disable camelcase */
|
||||||
export class RecaptchaAuthEntry extends React.Component {
|
interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
|
||||||
static LOGIN_TYPE = "m.login.recaptcha";
|
stageParams?: {
|
||||||
|
public_key?: string;
|
||||||
static propTypes = {
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
stageParams: PropTypes.object.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
busy: PropTypes.bool,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.RecaptchaAuthEntry")
|
||||||
|
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
|
||||||
|
static LOGIN_TYPE = AuthType.Recaptcha;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCaptchaResponse = response => {
|
private onCaptchaResponse = (response: string) => {
|
||||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
type: AuthType.Recaptcha,
|
||||||
response: response,
|
response: response,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -230,7 +265,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CaptchaForm sitePublicKey={sitePublicKey}
|
<CaptchaForm sitePublicKey={sitePublicKey}
|
||||||
onCaptchaResponse={this._onCaptchaResponse}
|
onCaptchaResponse={this.onCaptchaResponse}
|
||||||
/>
|
/>
|
||||||
{ errorSection }
|
{ errorSection }
|
||||||
</div>
|
</div>
|
||||||
|
@ -238,18 +273,28 @@ export class RecaptchaAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.TermsAuthEntry")
|
interface ITermsAuthEntryProps extends IAuthEntryProps {
|
||||||
export class TermsAuthEntry extends React.Component {
|
stageParams?: {
|
||||||
static LOGIN_TYPE = "m.login.terms";
|
policies?: Policies;
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
stageParams: PropTypes.object.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
busy: PropTypes.bool,
|
|
||||||
showContinue: PropTypes.bool,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
showContinue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalisedPolicyWithId extends LocalisedPolicy {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITermsAuthEntryState {
|
||||||
|
policies: LocalisedPolicyWithId[];
|
||||||
|
toggledPolicies: {
|
||||||
|
[policy: string]: boolean;
|
||||||
|
};
|
||||||
|
errorText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.TermsAuthEntry")
|
||||||
|
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
|
||||||
|
static LOGIN_TYPE = AuthType.Terms;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -294,8 +339,11 @@ export class TermsAuthEntry extends React.Component {
|
||||||
|
|
||||||
initToggles[policyId] = false;
|
initToggles[policyId] = false;
|
||||||
|
|
||||||
langPolicy.id = policyId;
|
pickedPolicies.push({
|
||||||
pickedPolicies.push(langPolicy);
|
id: policyId,
|
||||||
|
name: langPolicy.name,
|
||||||
|
url: langPolicy.url,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -311,11 +359,11 @@ export class TermsAuthEntry extends React.Component {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
tryContinue = () => {
|
public tryContinue = () => {
|
||||||
this._trySubmit();
|
this.trySubmit();
|
||||||
};
|
};
|
||||||
|
|
||||||
_togglePolicy(policyId) {
|
private togglePolicy(policyId: string) {
|
||||||
const newToggles = {};
|
const newToggles = {};
|
||||||
for (const policy of this.state.policies) {
|
for (const policy of this.state.policies) {
|
||||||
let checked = this.state.toggledPolicies[policy.id];
|
let checked = this.state.toggledPolicies[policy.id];
|
||||||
|
@ -326,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
this.setState({"toggledPolicies": newToggles});
|
this.setState({"toggledPolicies": newToggles});
|
||||||
}
|
}
|
||||||
|
|
||||||
_trySubmit = () => {
|
private trySubmit = () => {
|
||||||
let allChecked = true;
|
let allChecked = true;
|
||||||
for (const policy of this.state.policies) {
|
for (const policy of this.state.policies) {
|
||||||
const checked = this.state.toggledPolicies[policy.id];
|
const checked = this.state.toggledPolicies[policy.id];
|
||||||
|
@ -334,7 +382,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allChecked) {
|
if (allChecked) {
|
||||||
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
this.props.submitAuthDict({type: AuthType.Terms});
|
||||||
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
CountlyAnalytics.instance.track("onboarding_terms_complete");
|
||||||
} else {
|
} else {
|
||||||
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||||
|
@ -356,7 +404,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
checkboxes.push(
|
checkboxes.push(
|
||||||
// XXX: replace with StyledCheckbox
|
// XXX: replace with StyledCheckbox
|
||||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
<input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
|
||||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||||
</label>,
|
</label>,
|
||||||
);
|
);
|
||||||
|
@ -375,7 +423,7 @@ export class TermsAuthEntry extends React.Component {
|
||||||
if (this.props.showContinue !== false) {
|
if (this.props.showContinue !== false) {
|
||||||
// XXX: button classes
|
// XXX: button classes
|
||||||
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
|
||||||
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -389,21 +437,18 @@ export class TermsAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
|
||||||
export class EmailIdentityAuthEntry extends React.Component {
|
inputs?: {
|
||||||
static LOGIN_TYPE = "m.login.email.identity";
|
emailAddress?: string;
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
matrixClient: PropTypes.object.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
authSessionId: PropTypes.string.isRequired,
|
|
||||||
clientSecret: PropTypes.string.isRequired,
|
|
||||||
inputs: PropTypes.object.isRequired,
|
|
||||||
stageState: PropTypes.object.isRequired,
|
|
||||||
fail: PropTypes.func.isRequired,
|
|
||||||
setEmailSid: PropTypes.func.isRequired,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
stageState?: {
|
||||||
|
emailSid: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
|
||||||
|
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
|
||||||
|
static LOGIN_TYPE = AuthType.Email;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
|
@ -427,7 +472,7 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
{ emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||||
|
@ -437,37 +482,44 @@ export class EmailIdentityAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMsisdnAuthEntryProps extends IAuthEntryProps {
|
||||||
|
inputs: {
|
||||||
|
phoneCountry: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
};
|
||||||
|
clientSecret: string;
|
||||||
|
fail: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMsisdnAuthEntryState {
|
||||||
|
token: string;
|
||||||
|
requestingToken: boolean;
|
||||||
|
errorText: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
@replaceableComponent("views.auth.MsisdnAuthEntry")
|
||||||
export class MsisdnAuthEntry extends React.Component {
|
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps, IMsisdnAuthEntryState> {
|
||||||
static LOGIN_TYPE = "m.login.msisdn";
|
static LOGIN_TYPE = AuthType.Msisdn;
|
||||||
|
|
||||||
static propTypes = {
|
private submitUrl: string;
|
||||||
inputs: PropTypes.shape({
|
private sid: string;
|
||||||
phoneCountry: PropTypes.string,
|
private msisdn: string;
|
||||||
phoneNumber: PropTypes.string,
|
|
||||||
}),
|
|
||||||
fail: PropTypes.func,
|
|
||||||
clientSecret: PropTypes.func,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
matrixClient: PropTypes.object,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
token: '',
|
super(props);
|
||||||
requestingToken: false,
|
|
||||||
};
|
this.state = {
|
||||||
|
token: '',
|
||||||
|
requestingToken: false,
|
||||||
|
errorText: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
|
|
||||||
this._submitUrl = null;
|
|
||||||
this._sid = null;
|
|
||||||
this._msisdn = null;
|
|
||||||
this._tokenBox = null;
|
|
||||||
|
|
||||||
this.setState({requestingToken: true});
|
this.setState({requestingToken: true});
|
||||||
this._requestMsisdnToken().catch((e) => {
|
this.requestMsisdnToken().catch((e) => {
|
||||||
this.props.fail(e);
|
this.props.fail(e);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({requestingToken: false});
|
this.setState({requestingToken: false});
|
||||||
|
@ -477,26 +529,26 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
/*
|
/*
|
||||||
* Requests a verification token by SMS.
|
* Requests a verification token by SMS.
|
||||||
*/
|
*/
|
||||||
_requestMsisdnToken() {
|
private requestMsisdnToken(): Promise<void> {
|
||||||
return this.props.matrixClient.requestRegisterMsisdnToken(
|
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||||
this.props.inputs.phoneCountry,
|
this.props.inputs.phoneCountry,
|
||||||
this.props.inputs.phoneNumber,
|
this.props.inputs.phoneNumber,
|
||||||
this.props.clientSecret,
|
this.props.clientSecret,
|
||||||
1, // TODO: Multiple send attempts?
|
1, // TODO: Multiple send attempts?
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
this._submitUrl = result.submit_url;
|
this.submitUrl = result.submit_url;
|
||||||
this._sid = result.sid;
|
this.sid = result.sid;
|
||||||
this._msisdn = result.msisdn;
|
this.msisdn = result.msisdn;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTokenChange = e => {
|
private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
token: e.target.value,
|
token: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onFormSubmit = async e => {
|
private onFormSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.state.token == '') return;
|
if (this.state.token == '') return;
|
||||||
|
|
||||||
|
@ -506,20 +558,20 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (this._submitUrl) {
|
if (this.submitUrl) {
|
||||||
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
|
||||||
this._submitUrl, this._sid, this.props.clientSecret, this.state.token,
|
this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("The registration with MSISDN flow is misconfigured");
|
throw new Error("The registration with MSISDN flow is misconfigured");
|
||||||
}
|
}
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const creds = {
|
const creds = {
|
||||||
sid: this._sid,
|
sid: this.sid,
|
||||||
client_secret: this.props.clientSecret,
|
client_secret: this.props.clientSecret,
|
||||||
};
|
};
|
||||||
this.props.submitAuthDict({
|
this.props.submitAuthDict({
|
||||||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
type: AuthType.Msisdn,
|
||||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||||
// See https://github.com/vector-im/element-web/issues/10312
|
// See https://github.com/vector-im/element-web/issues/10312
|
||||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||||
|
@ -543,7 +595,7 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
} else {
|
} else {
|
||||||
const enableSubmit = Boolean(this.state.token);
|
const enableSubmit = Boolean(this.state.token);
|
||||||
const submitClasses = classnames({
|
const submitClasses = classNames({
|
||||||
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||||
mx_GeneralButton: true,
|
mx_GeneralButton: true,
|
||||||
});
|
});
|
||||||
|
@ -558,16 +610,16 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{ _t("A text message has been sent to %(msisdn)s",
|
<p>{ _t("A text message has been sent to %(msisdn)s",
|
||||||
{ msisdn: <i>{ this._msisdn }</i> },
|
{ msisdn: <i>{ this.msisdn }</i> },
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
<p>{ _t("Please enter the code it contains:") }</p>
|
<p>{ _t("Please enter the code it contains:") }</p>
|
||||||
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||||
value={this.state.token}
|
value={this.state.token}
|
||||||
onChange={this._onTokenChange}
|
onChange={this.onTokenChange}
|
||||||
aria-label={ _t("Code")}
|
aria-label={ _t("Code")}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
@ -584,40 +636,40 @@ export class MsisdnAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.SSOAuthEntry")
|
interface ISSOAuthEntryProps extends IAuthEntryProps {
|
||||||
export class SSOAuthEntry extends React.Component {
|
continueText?: string;
|
||||||
static propTypes = {
|
continueKind?: string;
|
||||||
matrixClient: PropTypes.object.isRequired,
|
onCancel?: () => void;
|
||||||
authSessionId: PropTypes.string.isRequired,
|
}
|
||||||
loginType: PropTypes.string.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
continueText: PropTypes.string,
|
|
||||||
continueKind: PropTypes.string,
|
|
||||||
onCancel: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static LOGIN_TYPE = "m.login.sso";
|
interface ISSOAuthEntryState {
|
||||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
phase: number;
|
||||||
|
attemptFailed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.auth.SSOAuthEntry")
|
||||||
|
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
|
||||||
|
static LOGIN_TYPE = AuthType.Sso;
|
||||||
|
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
|
||||||
|
|
||||||
static PHASE_PREAUTH = 1; // button to start SSO
|
static PHASE_PREAUTH = 1; // button to start SSO
|
||||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||||
|
|
||||||
_ssoUrl: string;
|
private ssoUrl: string;
|
||||||
|
private popupWindow: Window;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// We actually send the user through fallback auth so we don't have to
|
// We actually send the user through fallback auth so we don't have to
|
||||||
// deal with a redirect back to us, losing application context.
|
// deal with a redirect back to us, losing application context.
|
||||||
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||||
this.props.loginType,
|
this.props.loginType,
|
||||||
this.props.authSessionId,
|
this.props.authSessionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
window.addEventListener("message", this._onReceiveMessage);
|
window.addEventListener("message", this.onReceiveMessage);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||||
|
@ -625,44 +677,44 @@ export class SSOAuthEntry extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("message", this._onReceiveMessage);
|
window.removeEventListener("message", this.onReceiveMessage);
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attemptFailed = () => {
|
public attemptFailed = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
attemptFailed: true,
|
attemptFailed: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onReceiveMessage = event => {
|
private onReceiveMessage = (event: MessageEvent) => {
|
||||||
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onStartAuthClick = () => {
|
private onStartAuthClick = () => {
|
||||||
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
// Note: We don't use PlatformPeg's startSsoAuth functions because we almost
|
||||||
// certainly will need to open the thing in a new tab to avoid losing application
|
// certainly will need to open the thing in a new tab to avoid losing application
|
||||||
// context.
|
// context.
|
||||||
|
|
||||||
this._popupWindow = window.open(this._ssoUrl, "_blank");
|
this.popupWindow = window.open(this.ssoUrl, "_blank");
|
||||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||||
};
|
};
|
||||||
|
|
||||||
onConfirmClick = () => {
|
private onConfirmClick = () => {
|
||||||
this.props.submitAuthDict({});
|
this.props.submitAuthDict({});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -716,46 +768,37 @@ export class SSOAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.auth.FallbackAuthEntry")
|
@replaceableComponent("views.auth.FallbackAuthEntry")
|
||||||
export class FallbackAuthEntry extends React.Component {
|
export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
|
||||||
static propTypes = {
|
private popupWindow: Window;
|
||||||
matrixClient: PropTypes.object.isRequired,
|
private fallbackButton = createRef<HTMLAnchorElement>();
|
||||||
authSessionId: PropTypes.string.isRequired,
|
|
||||||
loginType: PropTypes.string.isRequired,
|
|
||||||
submitAuthDict: PropTypes.func.isRequired,
|
|
||||||
errorText: PropTypes.string,
|
|
||||||
onPhaseChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// we have to make the user click a button, as browsers will block
|
// we have to make the user click a button, as browsers will block
|
||||||
// the popup if we open it immediately.
|
// the popup if we open it immediately.
|
||||||
this._popupWindow = null;
|
this.popupWindow = null;
|
||||||
window.addEventListener("message", this._onReceiveMessage);
|
window.addEventListener("message", this.onReceiveMessage);
|
||||||
|
|
||||||
this._fallbackButton = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener("message", this._onReceiveMessage);
|
window.removeEventListener("message", this.onReceiveMessage);
|
||||||
if (this._popupWindow) {
|
if (this.popupWindow) {
|
||||||
this._popupWindow.close();
|
this.popupWindow.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus = () => {
|
public focus = () => {
|
||||||
if (this._fallbackButton.current) {
|
if (this.fallbackButton.current) {
|
||||||
this._fallbackButton.current.focus();
|
this.fallbackButton.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onShowFallbackClick = e => {
|
private onShowFallbackClick = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -763,10 +806,10 @@ export class FallbackAuthEntry extends React.Component {
|
||||||
this.props.loginType,
|
this.props.loginType,
|
||||||
this.props.authSessionId,
|
this.props.authSessionId,
|
||||||
);
|
);
|
||||||
this._popupWindow = window.open(url, "_blank");
|
this.popupWindow = window.open(url, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
_onReceiveMessage = event => {
|
private onReceiveMessage = (event: MessageEvent) => {
|
||||||
if (
|
if (
|
||||||
event.data === "authDone" &&
|
event.data === "authDone" &&
|
||||||
event.origin === this.props.matrixClient.getHomeserverUrl()
|
event.origin === this.props.matrixClient.getHomeserverUrl()
|
||||||
|
@ -786,27 +829,31 @@ export class FallbackAuthEntry extends React.Component {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
<a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
|
||||||
|
_t("Start authentication")
|
||||||
|
}</a>
|
||||||
{errorSection}
|
{errorSection}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthEntryComponents = [
|
export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
|
||||||
PasswordAuthEntry,
|
switch (loginType) {
|
||||||
RecaptchaAuthEntry,
|
case AuthType.Password:
|
||||||
EmailIdentityAuthEntry,
|
return PasswordAuthEntry;
|
||||||
MsisdnAuthEntry,
|
case AuthType.Recaptcha:
|
||||||
TermsAuthEntry,
|
return RecaptchaAuthEntry;
|
||||||
SSOAuthEntry,
|
case AuthType.Email:
|
||||||
];
|
return EmailIdentityAuthEntry;
|
||||||
|
case AuthType.Msisdn:
|
||||||
export default function getEntryComponentForLoginType(loginType) {
|
return MsisdnAuthEntry;
|
||||||
for (const c of AuthEntryComponents) {
|
case AuthType.Terms:
|
||||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
return TermsAuthEntry;
|
||||||
return c;
|
case AuthType.Sso:
|
||||||
}
|
case AuthType.SsoUnstable:
|
||||||
|
return SSOAuthEntry;
|
||||||
|
default:
|
||||||
|
return FallbackAuthEntry;
|
||||||
}
|
}
|
||||||
return FallbackAuthEntry;
|
|
||||||
}
|
}
|
|
@ -119,7 +119,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
||||||
if (this.props.room.roomId !== room.roomId) return;
|
if (this.props.room.roomId !== room.roomId) return;
|
||||||
|
|
||||||
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
|
||||||
this.setState({icon: this.calculateIcon()});
|
const newIcon = this.calculateIcon();
|
||||||
|
if (newIcon !== this.state.icon) {
|
||||||
|
this.setState({icon: newIcon});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState, useEffect} from 'react';
|
import React, { useState, useEffect, ChangeEvent, MouseEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PHASE_UNSENT,
|
PHASE_UNSENT,
|
||||||
|
@ -30,27 +30,33 @@ import {
|
||||||
PHASE_DONE,
|
PHASE_DONE,
|
||||||
PHASE_STARTED,
|
PHASE_STARTED,
|
||||||
PHASE_CANCELLED,
|
PHASE_CANCELLED,
|
||||||
|
VerificationRequest,
|
||||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import {SETTINGS} from "../../../settings/Settings";
|
import { SETTINGS } from "../../../settings/Settings";
|
||||||
import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
|
import SettingsStore, { LEVEL_ORDER } from "../../../settings/SettingsStore";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "./ErrorDialog";
|
import ErrorDialog from "./ErrorDialog";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { SettingLevel } from '../../../settings/SettingLevel';
|
||||||
|
|
||||||
class GenericEditor extends React.PureComponent {
|
interface IGenericEditorProps {
|
||||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
interface IGenericEditorState {
|
||||||
super(props);
|
message?: string;
|
||||||
this._onChange = this._onChange.bind(this);
|
[inputId: string]: boolean | string;
|
||||||
this.onBack = this.onBack.bind(this);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onBack() {
|
abstract class GenericEditor<
|
||||||
|
P extends IGenericEditorProps = IGenericEditorProps,
|
||||||
|
S extends IGenericEditorState = IGenericEditorState,
|
||||||
|
> extends React.PureComponent<P, S> {
|
||||||
|
protected onBack = () => {
|
||||||
if (this.state.message) {
|
if (this.state.message) {
|
||||||
this.setState({ message: null });
|
this.setState({ message: null });
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,47 +64,60 @@ class GenericEditor extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange(e) {
|
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
// @ts-ignore: Unsure how to convince TS this is okay when the state
|
||||||
|
// type can be extended.
|
||||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttons() {
|
protected abstract send();
|
||||||
|
|
||||||
|
protected buttons(): React.ReactNode {
|
||||||
return <div className="mx_Dialog_buttons">
|
return <div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
textInput(id, label) {
|
protected textInput(id: string, label: string): React.ReactNode {
|
||||||
return <Field
|
return <Field
|
||||||
id={id}
|
id={id}
|
||||||
label={label}
|
label={label}
|
||||||
size="42"
|
size={42}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
value={this.state[id]}
|
value={this.state[id] as string}
|
||||||
onChange={this._onChange}
|
onChange={this.onChange}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendCustomEvent extends GenericEditor {
|
interface ISendCustomEventProps extends IGenericEditorProps {
|
||||||
static getLabel() { return _t('Send Custom Event'); }
|
room: Room;
|
||||||
|
forceStateEvent?: boolean;
|
||||||
static propTypes = {
|
forceGeneralEvent?: boolean;
|
||||||
onBack: PropTypes.func.isRequired,
|
inputs?: {
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
eventType?: string;
|
||||||
forceStateEvent: PropTypes.bool,
|
stateKey?: string;
|
||||||
forceGeneralEvent: PropTypes.bool,
|
evContent?: string;
|
||||||
inputs: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISendCustomEventState extends IGenericEditorState {
|
||||||
|
isStateEvent: boolean;
|
||||||
|
eventType: string;
|
||||||
|
stateKey: string;
|
||||||
|
evContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendCustomEventState> {
|
||||||
|
static getLabel() { return _t('Send Custom Event'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
|
||||||
|
|
||||||
const {eventType, stateKey, evContent} = Object.assign({
|
const {eventType, stateKey, evContent} = Object.assign({
|
||||||
eventType: '',
|
eventType: '',
|
||||||
|
@ -115,7 +134,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
private doSend(content: object): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
if (this.state.isStateEvent) {
|
if (this.state.isStateEvent) {
|
||||||
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey);
|
||||||
|
@ -124,7 +143,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _send() {
|
protected send = async () => {
|
||||||
if (this.state.eventType === '') {
|
if (this.state.eventType === '') {
|
||||||
this.setState({ message: _t('You must specify an event type!') });
|
this.setState({ message: _t('You must specify an event type!') });
|
||||||
return;
|
return;
|
||||||
|
@ -133,7 +152,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
let message;
|
let message;
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.state.evContent);
|
const content = JSON.parse(this.state.evContent);
|
||||||
await this.send(content);
|
await this.doSend(content);
|
||||||
message = _t('Event sent!');
|
message = _t('Event sent!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||||
|
@ -147,7 +166,7 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ this.state.message }
|
{ this.state.message }
|
||||||
</div>
|
</div>
|
||||||
{ this._buttons() }
|
{ this.buttons() }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,35 +182,51 @@ export class SendCustomEvent extends GenericEditor {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ showTglFlip && <div style={{float: "right"}}>
|
{ showTglFlip && <div style={{float: "right"}}>
|
||||||
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isStateEvent} />
|
<input id="isStateEvent" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Event" data-tg-on="State Event" htmlFor="isStateEvent" />
|
type="checkbox"
|
||||||
|
checked={this.state.isStateEvent}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Event"
|
||||||
|
data-tg-on="State Event"
|
||||||
|
htmlFor="isStateEvent"
|
||||||
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SendAccountData extends GenericEditor {
|
interface ISendAccountDataProps extends IGenericEditorProps {
|
||||||
static getLabel() { return _t('Send Account Data'); }
|
room: Room;
|
||||||
|
isRoomAccountData: boolean;
|
||||||
static propTypes = {
|
forceMode: boolean;
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
inputs?: {
|
||||||
isRoomAccountData: PropTypes.bool,
|
eventType?: string;
|
||||||
forceMode: PropTypes.bool,
|
evContent?: string;
|
||||||
inputs: PropTypes.object,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISendAccountDataState extends IGenericEditorState {
|
||||||
|
isRoomAccountData: boolean;
|
||||||
|
eventType: string;
|
||||||
|
evContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountDataState> {
|
||||||
|
static getLabel() { return _t('Send Account Data'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._send = this._send.bind(this);
|
|
||||||
|
|
||||||
const {eventType, evContent} = Object.assign({
|
const {eventType, evContent} = Object.assign({
|
||||||
eventType: '',
|
eventType: '',
|
||||||
|
@ -206,7 +241,7 @@ class SendAccountData extends GenericEditor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
send(content) {
|
private doSend(content: object): Promise<void> {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
if (this.state.isRoomAccountData) {
|
if (this.state.isRoomAccountData) {
|
||||||
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content);
|
||||||
|
@ -214,7 +249,7 @@ class SendAccountData extends GenericEditor {
|
||||||
return cli.setAccountData(this.state.eventType, content);
|
return cli.setAccountData(this.state.eventType, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _send() {
|
protected send = async () => {
|
||||||
if (this.state.eventType === '') {
|
if (this.state.eventType === '') {
|
||||||
this.setState({ message: _t('You must specify an event type!') });
|
this.setState({ message: _t('You must specify an event type!') });
|
||||||
return;
|
return;
|
||||||
|
@ -223,7 +258,7 @@ class SendAccountData extends GenericEditor {
|
||||||
let message;
|
let message;
|
||||||
try {
|
try {
|
||||||
const content = JSON.parse(this.state.evContent);
|
const content = JSON.parse(this.state.evContent);
|
||||||
await this.send(content);
|
await this.doSend(content);
|
||||||
message = _t('Event sent!');
|
message = _t('Event sent!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
|
||||||
|
@ -237,7 +272,7 @@ class SendAccountData extends GenericEditor {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ this.state.message }
|
{ this.state.message }
|
||||||
</div>
|
</div>
|
||||||
{ this._buttons() }
|
{ this.buttons() }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,14 +282,23 @@ class SendAccountData extends GenericEditor {
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
<Field id="evContent" label={_t("Event Content")} type="text" className="mx_DevTools_textarea"
|
||||||
autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
|
autoComplete="off" value={this.state.evContent} onChange={this.onChange} element="textarea" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <button onClick={this._send}>{ _t('Send') }</button> }
|
{ !this.state.message && <button onClick={this.send}>{ _t('Send') }</button> }
|
||||||
{ !this.state.message && <div style={{float: "right"}}>
|
{ !this.state.message && <div style={{float: "right"}}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} disabled={this.props.forceMode} />
|
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
type="checkbox"
|
||||||
|
checked={this.state.isRoomAccountData}
|
||||||
|
disabled={this.props.forceMode}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Account Data"
|
||||||
|
data-tg-on="Room Data"
|
||||||
|
htmlFor="isRoomAccountData"
|
||||||
|
/>
|
||||||
</div> }
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -264,17 +308,22 @@ class SendAccountData extends GenericEditor {
|
||||||
const INITIAL_LOAD_TILES = 20;
|
const INITIAL_LOAD_TILES = 20;
|
||||||
const LOAD_TILES_STEP_SIZE = 50;
|
const LOAD_TILES_STEP_SIZE = 50;
|
||||||
|
|
||||||
class FilteredList extends React.PureComponent {
|
interface IFilteredListProps {
|
||||||
static propTypes = {
|
children: React.ReactElement[];
|
||||||
children: PropTypes.any,
|
query: string;
|
||||||
query: PropTypes.string,
|
onChange: (value: string) => void;
|
||||||
onChange: PropTypes.func,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static filterChildren(children, query) {
|
interface IFilteredListState {
|
||||||
|
filteredChildren: React.ReactElement[];
|
||||||
|
truncateAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FilteredList extends React.PureComponent<IFilteredListProps, IFilteredListState> {
|
||||||
|
static filterChildren(children: React.ReactElement[], query: string): React.ReactElement[] {
|
||||||
if (!query) return children;
|
if (!query) return children;
|
||||||
const lcQuery = query.toLowerCase();
|
const lcQuery = query.toLowerCase();
|
||||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
return children.filter((child) => child.key.toString().toLowerCase().includes(lcQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -295,27 +344,27 @@ class FilteredList extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showAll = () => {
|
private showAll = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
private createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||||
</button>;
|
</button>;
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuery = (ev) => {
|
private onQuery = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
getChildren = (start: number, end: number) => {
|
private getChildren = (start: number, end: number): React.ReactElement[] => {
|
||||||
return this.state.filteredChildren.slice(start, end);
|
return this.state.filteredChildren.slice(start, end);
|
||||||
};
|
};
|
||||||
|
|
||||||
getChildCount = (): number => {
|
private getChildCount = (): number => {
|
||||||
return this.state.filteredChildren.length;
|
return this.state.filteredChildren.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -336,28 +385,31 @@ class FilteredList extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomStateExplorer extends React.PureComponent {
|
interface IExplorerProps {
|
||||||
static getLabel() { return _t('Explore Room State'); }
|
room: Room;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
interface IRoomStateExplorerState {
|
||||||
onBack: PropTypes.func.isRequired,
|
eventType?: string;
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
event?: MatrixEvent;
|
||||||
};
|
editing: boolean;
|
||||||
|
queryEventType: string;
|
||||||
|
queryStateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateExplorerState> {
|
||||||
|
static getLabel() { return _t('Explore Room State'); }
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
private roomStateEvents: Map<string, Map<string, MatrixEvent>>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.roomStateEvents = this.props.room.currentState.events;
|
this.roomStateEvents = this.props.room.currentState.events;
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.editEv = this.editEv.bind(this);
|
|
||||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
|
||||||
this.onQueryStateKey = this.onQueryStateKey.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
eventType: null,
|
eventType: null,
|
||||||
event: null,
|
event: null,
|
||||||
|
@ -368,19 +420,19 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
browseEventType(eventType) {
|
private browseEventType(eventType: string) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ eventType });
|
this.setState({ eventType });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewSourceClick(event) {
|
private onViewSourceClick(event: MatrixEvent) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
this.setState({ editing: false });
|
this.setState({ editing: false });
|
||||||
} else if (this.state.event) {
|
} else if (this.state.event) {
|
||||||
|
@ -392,15 +444,15 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
editEv() {
|
private editEv = () => {
|
||||||
this.setState({ editing: true });
|
this.setState({ editing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryEventType(filterEventType) {
|
private onQueryEventType = (filterEventType: string) => {
|
||||||
this.setState({ queryEventType: filterEventType });
|
this.setState({ queryEventType: filterEventType });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryStateKey(filterStateKey) {
|
private onQueryStateKey = (filterStateKey: string) => {
|
||||||
this.setState({ queryStateKey: filterStateKey });
|
this.setState({ queryStateKey: filterStateKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,24 +524,22 @@ class RoomStateExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountDataExplorer extends React.PureComponent {
|
interface IAccountDataExplorerState {
|
||||||
static getLabel() { return _t('Explore Account Data'); }
|
isRoomAccountData: boolean;
|
||||||
|
event?: MatrixEvent;
|
||||||
|
editing: boolean;
|
||||||
|
queryEventType: string;
|
||||||
|
[inputId: string]: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
static propTypes = {
|
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||||
onBack: PropTypes.func.isRequired,
|
static getLabel() { return _t('Explore Account Data'); }
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.editEv = this.editEv.bind(this);
|
|
||||||
this._onChange = this._onChange.bind(this);
|
|
||||||
this.onQueryEventType = this.onQueryEventType.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRoomAccountData: false,
|
isRoomAccountData: false,
|
||||||
event: null,
|
event: null,
|
||||||
|
@ -499,20 +549,20 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
private getData(): Record<string, MatrixEvent> {
|
||||||
if (this.state.isRoomAccountData) {
|
if (this.state.isRoomAccountData) {
|
||||||
return this.props.room.accountData;
|
return this.props.room.accountData;
|
||||||
}
|
}
|
||||||
return this.context.store.accountData;
|
return this.context.store.accountData;
|
||||||
}
|
}
|
||||||
|
|
||||||
onViewSourceClick(event) {
|
private onViewSourceClick(event: MatrixEvent) {
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.state.editing) {
|
if (this.state.editing) {
|
||||||
this.setState({ editing: false });
|
this.setState({ editing: false });
|
||||||
} else if (this.state.event) {
|
} else if (this.state.event) {
|
||||||
|
@ -522,15 +572,15 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange(e) {
|
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
this.setState({[e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
editEv() {
|
private editEv = () => {
|
||||||
this.setState({ editing: true });
|
this.setState({ editing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryEventType(queryEventType) {
|
private onQueryEventType = (queryEventType: string) => {
|
||||||
this.setState({ queryEventType });
|
this.setState({ queryEventType });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,30 +630,39 @@ class AccountDataExplorer extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this.onBack}>{ _t('Back') }</button>
|
<button onClick={this.onBack}>{ _t('Back') }</button>
|
||||||
{ !this.state.message && <div style={{float: "right"}}>
|
<div style={{float: "right"}}>
|
||||||
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip" type="checkbox" onChange={this._onChange} checked={this.state.isRoomAccountData} />
|
<input id="isRoomAccountData" className="mx_DevTools_tgl mx_DevTools_tgl-flip"
|
||||||
<label className="mx_DevTools_tgl-btn" data-tg-off="Account Data" data-tg-on="Room Data" htmlFor="isRoomAccountData" />
|
type="checkbox"
|
||||||
</div> }
|
checked={this.state.isRoomAccountData}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
<label className="mx_DevTools_tgl-btn"
|
||||||
|
data-tg-off="Account Data"
|
||||||
|
data-tg-on="Room Data"
|
||||||
|
htmlFor="isRoomAccountData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServersInRoomList extends React.PureComponent {
|
interface IServersInRoomListState {
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRoomListState> {
|
||||||
static getLabel() { return _t('View Servers in Room'); }
|
static getLabel() { return _t('View Servers in Room'); }
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onBack: PropTypes.func.isRequired,
|
|
||||||
room: PropTypes.instanceOf(Room).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private servers: React.ReactElement[];
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const servers = new Set();
|
const servers = new Set<string>();
|
||||||
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1]));
|
||||||
this.servers = Array.from(servers).map(s =>
|
this.servers = Array.from(servers).map(s =>
|
||||||
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
<button key={s} className="mx_DevTools_ServersInRoomList_button">
|
||||||
|
@ -615,7 +674,7 @@ class ServersInRoomList extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onQuery = (query) => {
|
private onQuery = (query: string) => {
|
||||||
this.setState({ query });
|
this.setState({ query });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,7 +701,10 @@ const PHASE_MAP = {
|
||||||
[PHASE_CANCELLED]: "cancelled",
|
[PHASE_CANCELLED]: "cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
function VerificationRequest({txnId, request}) {
|
const VerificationRequestExplorer: React.FC<{
|
||||||
|
txnId: string;
|
||||||
|
request: VerificationRequest;
|
||||||
|
}> = ({txnId, request}) => {
|
||||||
const [, updateState] = useState();
|
const [, updateState] = useState();
|
||||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||||
|
|
||||||
|
@ -679,7 +741,7 @@ function VerificationRequest({txnId, request}) {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
class VerificationExplorer extends React.Component {
|
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Verification Requests");
|
return _t("Verification Requests");
|
||||||
}
|
}
|
||||||
|
@ -687,7 +749,7 @@ class VerificationExplorer extends React.Component {
|
||||||
/* Ensure this.context is the cli */
|
/* Ensure this.context is the cli */
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
onNewRequest = () => {
|
private onNewRequest = () => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,7 +772,7 @@ class VerificationExplorer extends React.Component {
|
||||||
return (<div>
|
return (<div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
<VerificationRequestExplorer txnId={txnId} request={request} key={txnId} />,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
@ -720,7 +782,12 @@ class VerificationExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WidgetExplorer extends React.Component {
|
interface IWidgetExplorerState {
|
||||||
|
query: string;
|
||||||
|
editWidget?: IApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WidgetExplorer extends React.Component<IExplorerProps, IWidgetExplorerState> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Active Widgets");
|
return _t("Active Widgets");
|
||||||
}
|
}
|
||||||
|
@ -734,19 +801,19 @@ class WidgetExplorer extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onWidgetStoreUpdate = () => {
|
private onWidgetStoreUpdate = () => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
onQueryChange = (query) => {
|
private onQueryChange = (query: string) => {
|
||||||
this.setState({query});
|
this.setState({query});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditWidget = (widget) => {
|
private onEditWidget = (widget: IApp) => {
|
||||||
this.setState({editWidget: widget});
|
this.setState({editWidget: widget});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBack = () => {
|
private onBack = () => {
|
||||||
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
const widgets = WidgetStore.instance.getApps(this.props.room.roomId);
|
||||||
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
if (this.state.editWidget && widgets.includes(this.state.editWidget)) {
|
||||||
this.setState({editWidget: null});
|
this.setState({editWidget: null});
|
||||||
|
@ -769,8 +836,11 @@ class WidgetExplorer extends React.Component {
|
||||||
const editWidget = this.state.editWidget;
|
const editWidget = this.state.editWidget;
|
||||||
const widgets = WidgetStore.instance.getApps(room.roomId);
|
const widgets = WidgetStore.instance.getApps(room.roomId);
|
||||||
if (editWidget && widgets.includes(editWidget)) {
|
if (editWidget && widgets.includes(editWidget)) {
|
||||||
const allState = Array.from(Array.from(room.currentState.events.values()).map(e => e.values()))
|
const allState = Array.from(
|
||||||
.reduce((p, c) => {p.push(...c); return p;}, []);
|
Array.from(room.currentState.events.values()).map((e: Map<string, MatrixEvent>) => {
|
||||||
|
return e.values();
|
||||||
|
}),
|
||||||
|
).reduce((p, c) => { p.push(...c); return p; }, []);
|
||||||
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
const stateEv = allState.find(ev => ev.getId() === editWidget.eventId);
|
||||||
if (!stateEv) { // "should never happen"
|
if (!stateEv) { // "should never happen"
|
||||||
return <div>
|
return <div>
|
||||||
|
@ -811,7 +881,15 @@ class WidgetExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsExplorer extends React.Component {
|
interface ISettingsExplorerState {
|
||||||
|
query: string;
|
||||||
|
editSetting?: string;
|
||||||
|
viewSetting?: string;
|
||||||
|
explicitValues?: string;
|
||||||
|
explicitRoomValues?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsExplorer extends React.PureComponent<IExplorerProps, ISettingsExplorerState> {
|
||||||
static getLabel() {
|
static getLabel() {
|
||||||
return _t("Settings Explorer");
|
return _t("Settings Explorer");
|
||||||
}
|
}
|
||||||
|
@ -829,19 +907,19 @@ class SettingsExplorer extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onQueryChange = (ev) => {
|
private onQueryChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({query: ev.target.value});
|
this.setState({query: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onExplValuesEdit = (ev) => {
|
private onExplValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
this.setState({explicitValues: ev.target.value});
|
this.setState({explicitValues: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onExplRoomValuesEdit = (ev) => {
|
private onExplRoomValuesEdit = (ev: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
this.setState({explicitRoomValues: ev.target.value});
|
this.setState({explicitRoomValues: ev.target.value});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBack = () => {
|
private onBack = () => {
|
||||||
if (this.state.editSetting) {
|
if (this.state.editSetting) {
|
||||||
this.setState({editSetting: null});
|
this.setState({editSetting: null});
|
||||||
} else if (this.state.viewSetting) {
|
} else if (this.state.viewSetting) {
|
||||||
|
@ -851,12 +929,12 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewClick = (ev, settingId) => {
|
private onViewClick = (ev: MouseEvent, settingId: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.setState({viewSetting: settingId});
|
this.setState({viewSetting: settingId});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditClick = (ev, settingId) => {
|
private onEditClick = (ev: MouseEvent, settingId: string) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.setState({
|
this.setState({
|
||||||
editSetting: settingId,
|
editSetting: settingId,
|
||||||
|
@ -865,7 +943,7 @@ class SettingsExplorer extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSaveClick = async () => {
|
private onSaveClick = async () => {
|
||||||
try {
|
try {
|
||||||
const settingId = this.state.editSetting;
|
const settingId = this.state.editSetting;
|
||||||
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
const parsedExplicit = JSON.parse(this.state.explicitValues);
|
||||||
|
@ -874,7 +952,7 @@ class SettingsExplorer extends React.Component {
|
||||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
|
||||||
try {
|
try {
|
||||||
const val = parsedExplicit[level];
|
const val = parsedExplicit[level];
|
||||||
await SettingsStore.setValue(settingId, null, level, val);
|
await SettingsStore.setValue(settingId, null, level as SettingLevel, val);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
|
@ -884,7 +962,7 @@ class SettingsExplorer extends React.Component {
|
||||||
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
|
||||||
try {
|
try {
|
||||||
const val = parsedExplicitRoom[level];
|
const val = parsedExplicitRoom[level];
|
||||||
await SettingsStore.setValue(settingId, roomId, level, val);
|
await SettingsStore.setValue(settingId, roomId, level as SettingLevel, val);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
|
@ -901,7 +979,7 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSettingValue(val) {
|
private renderSettingValue(val: any): string {
|
||||||
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
// Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
|
||||||
const toStringTypes = ['boolean', 'number'];
|
const toStringTypes = ['boolean', 'number'];
|
||||||
if (toStringTypes.includes(typeof(val))) {
|
if (toStringTypes.includes(typeof(val))) {
|
||||||
|
@ -911,7 +989,7 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExplicitSettingValues(setting, roomId) {
|
private renderExplicitSettingValues(setting: string, roomId: string): string {
|
||||||
const vals = {};
|
const vals = {};
|
||||||
for (const level of LEVEL_ORDER) {
|
for (const level of LEVEL_ORDER) {
|
||||||
try {
|
try {
|
||||||
|
@ -926,7 +1004,7 @@ class SettingsExplorer extends React.Component {
|
||||||
return JSON.stringify(vals, null, 4);
|
return JSON.stringify(vals, null, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCanEditLevel(roomId, level) {
|
private renderCanEditLevel(roomId: string, level: SettingLevel): React.ReactNode {
|
||||||
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
|
||||||
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
|
||||||
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
return <td className={className}><code>{canEdit.toString()}</code></td>;
|
||||||
|
@ -1062,27 +1140,37 @@ class SettingsExplorer extends React.Component {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Value:")}
|
{_t("Value:")}
|
||||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}</code>
|
<code>{this.renderSettingValue(
|
||||||
|
SettingsStore.getValue(this.state.viewSetting),
|
||||||
|
)}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Value in this room:")}
|
{_t("Value in this room:")}
|
||||||
<code>{this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}</code>
|
<code>{this.renderSettingValue(
|
||||||
|
SettingsStore.getValue(this.state.viewSetting, room.roomId),
|
||||||
|
)}</code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Values at explicit levels:")}
|
{_t("Values at explicit levels:")}
|
||||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, null)}</code></pre>
|
<pre><code>{this.renderExplicitSettingValues(
|
||||||
|
this.state.viewSetting, null,
|
||||||
|
)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{_t("Values at explicit levels in this room:")}
|
{_t("Values at explicit levels in this room:")}
|
||||||
<pre><code>{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}</code></pre>
|
<pre><code>{this.renderExplicitSettingValues(
|
||||||
|
this.state.viewSetting, room.roomId,
|
||||||
|
)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}</button>
|
<button onClick={(e) => this.onEditClick(e, this.state.viewSetting)}>{
|
||||||
|
_t("Edit Values")
|
||||||
|
}</button>
|
||||||
<button onClick={this.onBack}>{_t("Back")}</button>
|
<button onClick={this.onBack}>{_t("Back")}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1091,7 +1179,11 @@ class SettingsExplorer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Entries = [
|
type DevtoolsDialogEntry = React.JSXElementConstructor<any> & {
|
||||||
|
getLabel: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Entries: DevtoolsDialogEntry[] = [
|
||||||
SendCustomEvent,
|
SendCustomEvent,
|
||||||
RoomStateExplorer,
|
RoomStateExplorer,
|
||||||
SendAccountData,
|
SendAccountData,
|
||||||
|
@ -1102,43 +1194,36 @@ const Entries = [
|
||||||
SettingsExplorer,
|
SettingsExplorer,
|
||||||
];
|
];
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
interface IProps {
|
||||||
export default class DevtoolsDialog extends React.PureComponent {
|
roomId: string;
|
||||||
static propTypes = {
|
onFinished: (finished: boolean) => void;
|
||||||
roomId: PropTypes.string.isRequired,
|
}
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
mode?: DevtoolsDialogEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.DevtoolsDialog")
|
||||||
|
export default class DevtoolsDialog extends React.PureComponent<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onBack = this.onBack.bind(this);
|
|
||||||
this.onCancel = this.onCancel.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mode: null,
|
mode: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
private setMode(mode: DevtoolsDialogEntry) {
|
||||||
this._unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setMode(mode) {
|
|
||||||
return () => {
|
return () => {
|
||||||
this.setState({ mode });
|
this.setState({ mode });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack = () => {
|
||||||
if (this.prevMode) {
|
this.setState({ mode: null });
|
||||||
this.setState({ mode: this.prevMode });
|
|
||||||
this.prevMode = null;
|
|
||||||
} else {
|
|
||||||
this.setState({ mode: null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCancel() {
|
private onCancel = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1165,7 +1250,7 @@ export default class DevtoolsDialog extends React.PureComponent {
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
{ Entries.map((Entry) => {
|
{ Entries.map((Entry) => {
|
||||||
const label = Entry.getLabel();
|
const label = Entry.getLabel();
|
||||||
const onClick = this._setMode(Entry);
|
const onClick = this.setMode(Entry);
|
||||||
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
return <button className={classes} key={label} onClick={onClick}>{ label }</button>;
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
|
@ -30,7 +30,6 @@ import ToggleSwitch from "../elements/ToggleSwitch";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {allSettled} from "../../../utils/promise";
|
|
||||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
|
|
||||||
|
@ -91,7 +90,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
const failures = results.filter(r => r.status === "rejected");
|
const failures = results.filter(r => r.status === "rejected");
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
|
|
|
@ -38,13 +38,14 @@ import withValidation from "../elements/Validation";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import TextInputDialog from "../dialogs/TextInputDialog";
|
import TextInputDialog from "../dialogs/TextInputDialog";
|
||||||
import QuestionDialog from "../dialogs/QuestionDialog";
|
import QuestionDialog from "../dialogs/QuestionDialog";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
export const ALL_ROOMS = Symbol("ALL_ROOMS");
|
||||||
|
|
||||||
const SETTING_NAME = "room_directory_servers";
|
const SETTING_NAME = "room_directory_servers";
|
||||||
|
|
||||||
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
const inPlaceOf = (elementRect: Pick<DOMRect, "right" | "top">) => ({
|
||||||
right: window.innerWidth - elementRect.right,
|
right: UIStore.instance.windowWidth - elementRect.right,
|
||||||
top: elementRect.top,
|
top: elementRect.top,
|
||||||
chevronOffset: 0,
|
chevronOffset: 0,
|
||||||
chevronFace: ChevronFace.None,
|
chevronFace: ChevronFace.None,
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
import ICanvasEffect from '../../../effects/ICanvasEffect';
|
||||||
import {CHAT_EFFECTS} from '../../../effects'
|
import { CHAT_EFFECTS } from '../../../effects'
|
||||||
|
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomWidth: number;
|
roomWidth: number;
|
||||||
|
@ -45,8 +46,8 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
if (canvasRef.current) {
|
if (canvasRef.current && canvasRef.current?.height !== UIStore.instance.windowHeight) {
|
||||||
canvasRef.current.height = window.innerHeight;
|
canvasRef.current.height = UIStore.instance.windowHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onAction = (payload: { action: string }) => {
|
const onAction = (payload: { action: string }) => {
|
||||||
|
@ -58,12 +59,12 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
|
||||||
}
|
}
|
||||||
const dispatcherRef = dis.register(onAction);
|
const dispatcherRef = dis.register(onAction);
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = UIStore.instance.windowHeight;
|
||||||
window.addEventListener('resize', resize, true);
|
UIStore.instance.on(UI_EVENTS.Resize, resize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dis.unregister(dispatcherRef);
|
dis.unregister(dispatcherRef);
|
||||||
window.removeEventListener('resize', resize);
|
UIStore.instance.off(UI_EVENTS.Resize, resize);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored
|
||||||
for (const effect in currentEffects) {
|
for (const effect in currentEffects) {
|
||||||
|
|
|
@ -18,19 +18,29 @@ import React from "react";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.InlineSpinner")
|
interface IProps {
|
||||||
export default class InlineSpinner extends React.Component {
|
w?: number;
|
||||||
render() {
|
h?: number;
|
||||||
const w = this.props.w || 16;
|
children?: React.ReactNode;
|
||||||
const h = this.props.h || 16;
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.InlineSpinner")
|
||||||
|
export default class InlineSpinner extends React.PureComponent<IProps> {
|
||||||
|
static defaultProps = {
|
||||||
|
w: 16,
|
||||||
|
h: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_InlineSpinner">
|
<div className="mx_InlineSpinner">
|
||||||
<div
|
<div
|
||||||
className="mx_InlineSpinner_icon mx_Spinner_icon"
|
className="mx_InlineSpinner_icon mx_Spinner_icon"
|
||||||
style={{width: w, height: h}}
|
style={{width: this.props.w, height: this.props.h}}
|
||||||
aria-label={_t("Loading...")}
|
aria-label={_t("Loading...")}
|
||||||
></div>
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -22,6 +22,7 @@ import React, {Component, CSSProperties} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
const MIN_TOOLTIP_HEIGHT = 25;
|
const MIN_TOOLTIP_HEIGHT = 25;
|
||||||
|
|
||||||
|
@ -97,15 +98,15 @@ export default class Tooltip extends React.Component<IProps> {
|
||||||
// we need so that we're still centered.
|
// we need so that we're still centered.
|
||||||
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
|
||||||
}
|
}
|
||||||
|
const width = UIStore.instance.windowWidth;
|
||||||
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
|
const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
|
||||||
const top = baseTop + offset;
|
const top = baseTop + offset;
|
||||||
const right = window.innerWidth - parentBox.right - window.pageXOffset - 16;
|
const right = width - parentBox.right - window.pageXOffset - 16;
|
||||||
const left = parentBox.right + window.pageXOffset + 6;
|
const left = parentBox.right + window.pageXOffset + 6;
|
||||||
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
|
const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2);
|
||||||
switch (this.props.alignment) {
|
switch (this.props.alignment) {
|
||||||
case Alignment.Natural:
|
case Alignment.Natural:
|
||||||
if (parentBox.right > window.innerWidth / 2) {
|
if (parentBox.right > width / 2) {
|
||||||
style.right = right;
|
style.right = right;
|
||||||
style.top = top;
|
style.top = top;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -19,19 +19,30 @@ import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.TooltipButton")
|
interface IProps {
|
||||||
export default class TooltipButton extends React.Component {
|
helpText: string;
|
||||||
state = {
|
}
|
||||||
hover: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseOver = () => {
|
interface IState {
|
||||||
|
hover: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.TooltipButton")
|
||||||
|
export default class TooltipButton extends React.Component<IProps, IState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseOver = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
hover: true,
|
hover: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMouseLeave = () => {
|
private onMouseLeave = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
hover: false,
|
hover: false,
|
||||||
});
|
});
|
|
@ -71,10 +71,14 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
||||||
|
|
||||||
// We should have a buffer to work with now: let's set it up
|
// We should have a buffer to work with now: let's set it up
|
||||||
const playback = new Playback(buffer, waveform);
|
const playback = new Playback(buffer, waveform);
|
||||||
this.setState({playback});
|
this.setState({ playback });
|
||||||
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.state.playback?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
// TODO: @@TR: Verify error state
|
// TODO: @@TR: Verify error state
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {toRightOf} from "../../structures/ContextMenu";
|
||||||
import {copyPlaintext} from "../../../utils/strings";
|
import {copyPlaintext} from "../../../utils/strings";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
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 {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ export default class TextualBody extends React.Component {
|
||||||
_addCodeExpansionButton(div, pre) {
|
_addCodeExpansionButton(div, pre) {
|
||||||
// Calculate how many percent does the pre element take up.
|
// Calculate how many percent does the pre element take up.
|
||||||
// If it's less than 30% we don't add the expansion button.
|
// If it's less than 30% we don't add the expansion button.
|
||||||
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
|
const percentageOfViewport = pre.offsetHeight / UIStore.instance.windowHeight * 100;
|
||||||
if (percentageOfViewport < 30) return;
|
if (percentageOfViewport < 30) return;
|
||||||
|
|
||||||
const button = document.createElement("span");
|
const button = document.createElement("span");
|
||||||
|
|
|
@ -46,6 +46,7 @@ import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
import {useRoomMemberCount} from "../../../hooks/useRoomMembers";
|
||||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -116,8 +117,8 @@ const AppRow: React.FC<IAppRowProps> = ({ app, room }) => {
|
||||||
const rect = handle.current.getBoundingClientRect();
|
const rect = handle.current.getBoundingClientRect();
|
||||||
contextMenu = <WidgetContextMenu
|
contextMenu = <WidgetContextMenu
|
||||||
chevronFace={ChevronFace.None}
|
chevronFace={ChevronFace.None}
|
||||||
right={window.innerWidth - rect.right}
|
right={UIStore.instance.windowWidth - rect.right}
|
||||||
bottom={window.innerHeight - rect.top}
|
bottom={UIStore.instance.windowHeight - rect.top}
|
||||||
onFinished={closeMenu}
|
onFinished={closeMenu}
|
||||||
app={app}
|
app={app}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -66,6 +66,7 @@ import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRight
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
|
import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
|
@ -1449,8 +1450,8 @@ const UserInfoHeader: React.FC<{
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||||
member={member}
|
member={member}
|
||||||
width={2 * 0.3 * window.innerHeight} // 2x@30vh
|
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||||
height={2 * 0.3 * window.innerHeight} // 2x@30vh
|
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||||
resizeMethod="scale"
|
resizeMethod="scale"
|
||||||
fallbackUserId={member.userId}
|
fallbackUserId={member.userId}
|
||||||
onClick={onMemberAvatarClick}
|
onClick={onMemberAvatarClick}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { Action } from "../../../dispatcher/actions";
|
||||||
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -65,7 +66,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<WidgetContextMenu
|
<WidgetContextMenu
|
||||||
chevronFace={ChevronFace.None}
|
chevronFace={ChevronFace.None}
|
||||||
right={window.innerWidth - rect.right - 12}
|
right={UIStore.instance.windowWidth - rect.right - 12}
|
||||||
top={rect.bottom + 12}
|
top={rect.bottom + 12}
|
||||||
onFinished={closeMenu}
|
onFinished={closeMenu}
|
||||||
app={app}
|
app={app}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayout
|
||||||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.AppsDrawer")
|
@replaceableComponent("views.rooms.AppsDrawer")
|
||||||
export default class AppsDrawer extends React.Component {
|
export default class AppsDrawer extends React.Component {
|
||||||
|
@ -290,7 +291,7 @@ const PersistentVResizer = ({
|
||||||
|
|
||||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||||
if (!minHeight) minHeight = 100;
|
if (!minHeight) minHeight = 100;
|
||||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
if (!maxHeight) maxHeight = (UIStore.instance.windowHeight / 4) * 3;
|
||||||
|
|
||||||
// Convert from percentage to height. Note that the default height is 280px.
|
// Convert from percentage to height. Note that the default height is 280px.
|
||||||
if (defaultHeight) {
|
if (defaultHeight) {
|
||||||
|
|
|
@ -55,7 +55,6 @@ interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
onFocus: (ev: React.FocusEvent) => void;
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
onResize: () => void;
|
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
activeSpace: Room;
|
activeSpace: Room;
|
||||||
|
@ -404,9 +403,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
const newSublists = objectWithOnly(newLists, newListIds);
|
const newSublists = objectWithOnly(newLists, newListIds);
|
||||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||||
|
|
||||||
this.setState({sublists, isNameFiltering}, () => {
|
this.setState({sublists, isNameFiltering});
|
||||||
this.props.onResize();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -537,7 +534,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.props.onResize}
|
|
||||||
showSkeleton={showSkeleton}
|
showSkeleton={showSkeleton}
|
||||||
extraTiles={extraTiles}
|
extraTiles={extraTiles}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
|
||||||
const RoomListNumResults: React.FC = () => {
|
interface IProps {
|
||||||
|
onVisibilityChange?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||||
const [count, setCount] = useState<number>(null);
|
const [count, setCount] = useState<number>(null);
|
||||||
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||||
|
@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onVisibilityChange) {
|
||||||
|
onVisibilityChange();
|
||||||
|
}
|
||||||
|
}, [count, onVisibilityChange]);
|
||||||
|
|
||||||
if (typeof count !== "number") return null;
|
if (typeof count !== "number") return null;
|
||||||
|
|
||||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||||
|
|
|
@ -74,7 +74,6 @@ interface IProps {
|
||||||
addRoomLabel: string;
|
addRoomLabel: string;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
onResize: () => void;
|
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
alwaysVisible?: boolean;
|
alwaysVisible?: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -473,7 +472,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private toggleCollapsed = () => {
|
private toggleCollapsed = () => {
|
||||||
this.layout.isCollapsed = this.state.isExpanded;
|
this.layout.isCollapsed = this.state.isExpanded;
|
||||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||||
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
@ -530,7 +528,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
tiles.push(<RoomTile
|
tiles.push(<RoomTile
|
||||||
room={room}
|
room={room}
|
||||||
key={`room-${room.roomId}`}
|
key={`room-${room.roomId}`}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
|
||||||
showMessagePreview={this.layout.showPreviews}
|
showMessagePreview={this.layout.showPreviews}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
tag={this.props.tagId}
|
tag={this.props.tagId}
|
||||||
|
|
|
@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
showMessagePreview: boolean;
|
showMessagePreview: boolean;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tag: TagID;
|
tag: TagID;
|
||||||
resizeNotifier: ResizeNotifier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||||
|
@ -106,9 +104,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||||
if (this.props.resizeNotifier) {
|
|
||||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private countUnsentEvents(): number {
|
private countUnsentEvents(): number {
|
||||||
|
@ -123,12 +118,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
this.forceUpdate(); // notification state changed - update
|
this.forceUpdate(); // notification state changed - update
|
||||||
};
|
};
|
||||||
|
|
||||||
private onResize = () => {
|
|
||||||
if (this.showMessagePreview && !this.state.messagePreview) {
|
|
||||||
this.generatePreview();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
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});
|
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
|
||||||
|
@ -148,7 +137,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
||||||
|
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||||
|
if (showMessageChanged || minimizedChanged) {
|
||||||
this.generatePreview();
|
this.generatePreview();
|
||||||
}
|
}
|
||||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||||
|
@ -208,9 +199,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||||
);
|
);
|
||||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||||
}
|
}
|
||||||
if (this.props.resizeNotifier) {
|
|
||||||
this.props.resizeNotifier.off("middlePanelResized", this.onResize);
|
|
||||||
}
|
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||||
|
|
|
@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500;
|
||||||
const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.Stickerpicker")
|
@replaceableComponent("views.rooms.Stickerpicker")
|
||||||
export default class Stickerpicker extends React.Component {
|
export default class Stickerpicker extends React.PureComponent {
|
||||||
static currentWidget;
|
static currentWidget;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -341,21 +341,27 @@ export default class Stickerpicker extends React.Component {
|
||||||
* @param {Event} ev Event that triggered the function call
|
* @param {Event} ev Event that triggered the function call
|
||||||
*/
|
*/
|
||||||
_onHideStickersClick(ev) {
|
_onHideStickersClick(ev) {
|
||||||
this.setState({showStickers: false});
|
if (this.state.showStickers) {
|
||||||
|
this.setState({showStickers: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the window is resized
|
* Called when the window is resized
|
||||||
*/
|
*/
|
||||||
_onResize() {
|
_onResize() {
|
||||||
this.setState({showStickers: false});
|
if (this.state.showStickers) {
|
||||||
|
this.setState({showStickers: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stickers picker was hidden
|
* The stickers picker was hidden
|
||||||
*/
|
*/
|
||||||
_onFinished() {
|
_onFinished() {
|
||||||
this.setState({showStickers: false});
|
if (this.state.showStickers) {
|
||||||
|
this.setState({showStickers: false});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,36 +16,44 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 * as WhoIsTyping from '../../../WhoIsTyping';
|
import * as WhoIsTyping from '../../../WhoIsTyping';
|
||||||
import Timer from '../../../utils/Timer';
|
import Timer from '../../../utils/Timer';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// the room this statusbar is representing.
|
||||||
|
room: Room;
|
||||||
|
onShown?: () => void;
|
||||||
|
onHidden?: () => void;
|
||||||
|
// Number of names to display in typing indication. E.g. set to 3, will
|
||||||
|
// result in "X, Y, Z and 100 others are typing."
|
||||||
|
whoIsTypingLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
usersTyping: RoomMember[];
|
||||||
|
// a map with userid => Timer to delay
|
||||||
|
// hiding the "x is typing" message for a
|
||||||
|
// user so hiding it can coincide
|
||||||
|
// with the sent message by the other side
|
||||||
|
// resulting in less timeline jumpiness
|
||||||
|
delayedStopTypingTimers: Record<string, Timer>;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.WhoIsTypingTile")
|
@replaceableComponent("views.rooms.WhoIsTypingTile")
|
||||||
export default class WhoIsTypingTile extends React.Component {
|
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
// the room this statusbar is representing.
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
onShown: PropTypes.func,
|
|
||||||
onHidden: PropTypes.func,
|
|
||||||
// Number of names to display in typing indication. E.g. set to 3, will
|
|
||||||
// result in "X, Y, Z and 100 others are typing."
|
|
||||||
whoIsTypingLimit: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
whoIsTypingLimit: 3,
|
whoIsTypingLimit: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||||
// a map with userid => Timer to delay
|
|
||||||
// hiding the "x is typing" message for a
|
|
||||||
// user so hiding it can coincide
|
|
||||||
// with the sent message by the other side
|
|
||||||
// resulting in less timeline jumpiness
|
|
||||||
delayedStopTypingTimers: {},
|
delayedStopTypingTimers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,37 +79,39 @@ export default class WhoIsTypingTile extends React.Component {
|
||||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
}
|
}
|
||||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
|
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
|
||||||
}
|
}
|
||||||
|
|
||||||
_isVisible(state) {
|
private _isVisible(state: IState): boolean {
|
||||||
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isVisible = () => {
|
public isVisible = (): boolean => {
|
||||||
return this._isVisible(this.state);
|
return this._isVisible(this.state);
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomTimeline = (event, room) => {
|
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {
|
||||||
if (room?.roomId === this.props.room?.roomId) {
|
if (room?.roomId === this.props.room?.roomId) {
|
||||||
const userId = event.getSender();
|
const userId = event.getSender();
|
||||||
// remove user from usersTyping
|
// remove user from usersTyping
|
||||||
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
|
||||||
this.setState({usersTyping});
|
if (usersTyping.length !== this.state.usersTyping.length) {
|
||||||
|
this.setState({usersTyping});
|
||||||
|
}
|
||||||
// abort timer if any
|
// abort timer if any
|
||||||
this._abortUserTimer(userId);
|
this.abortUserTimer(userId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomMemberTyping = (ev, member) => {
|
private onRoomMemberTyping = (): void => {
|
||||||
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
|
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
|
||||||
this.setState({
|
this.setState({
|
||||||
delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping),
|
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
|
||||||
usersTyping,
|
usersTyping,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateDelayedStopTypingTimers(usersTyping) {
|
private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record<string, Timer> {
|
||||||
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
|
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
|
||||||
return !usersTyping.some((b) => a.userId === b.userId);
|
return !usersTyping.some((b) => a.userId === b.userId);
|
||||||
});
|
});
|
||||||
|
@ -129,7 +139,7 @@ export default class WhoIsTypingTile extends React.Component {
|
||||||
delayedStopTypingTimers[m.userId] = timer;
|
delayedStopTypingTimers[m.userId] = timer;
|
||||||
timer.start();
|
timer.start();
|
||||||
timer.finished().then(
|
timer.finished().then(
|
||||||
() => this._removeUserTimer(m.userId), // on elapsed
|
() => this.removeUserTimer(m.userId), // on elapsed
|
||||||
() => {/* aborted */},
|
() => {/* aborted */},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,15 +149,15 @@ export default class WhoIsTypingTile extends React.Component {
|
||||||
return delayedStopTypingTimers;
|
return delayedStopTypingTimers;
|
||||||
}
|
}
|
||||||
|
|
||||||
_abortUserTimer(userId) {
|
private abortUserTimer(userId: string): void {
|
||||||
const timer = this.state.delayedStopTypingTimers[userId];
|
const timer = this.state.delayedStopTypingTimers[userId];
|
||||||
if (timer) {
|
if (timer) {
|
||||||
timer.abort();
|
timer.abort();
|
||||||
this._removeUserTimer(userId);
|
this.removeUserTimer(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeUserTimer(userId) {
|
private removeUserTimer(userId: string): void {
|
||||||
const timer = this.state.delayedStopTypingTimers[userId];
|
const timer = this.state.delayedStopTypingTimers[userId];
|
||||||
if (timer) {
|
if (timer) {
|
||||||
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
|
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
|
||||||
|
@ -156,7 +166,7 @@ export default class WhoIsTypingTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderTypingIndicatorAvatars(users, limit) {
|
private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] {
|
||||||
let othersCount = 0;
|
let othersCount = 0;
|
||||||
if (users.length > limit) {
|
if (users.length > limit) {
|
||||||
othersCount = users.length - limit + 1;
|
othersCount = users.length - limit + 1;
|
||||||
|
@ -210,7 +220,7 @@ export default class WhoIsTypingTile extends React.Component {
|
||||||
return (
|
return (
|
||||||
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||||
<div className="mx_WhoIsTypingTile_avatars">
|
<div className="mx_WhoIsTypingTile_avatars">
|
||||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_WhoIsTypingTile_label">
|
<div className="mx_WhoIsTypingTile_label">
|
||||||
{ typingString }
|
{ typingString }
|
|
@ -34,6 +34,7 @@ import { isJoinedOrNearlyJoined } from "./utils/membership";
|
||||||
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
|
||||||
import SpaceStore from "./stores/SpaceStore";
|
import SpaceStore from "./stores/SpaceStore";
|
||||||
import { makeSpaceParentEvent } from "./utils/space";
|
import { makeSpaceParentEvent } from "./utils/space";
|
||||||
|
import { Action } from "./dispatcher/actions"
|
||||||
|
|
||||||
// we define a number of interfaces which take their names from the js-sdk
|
// we define a number of interfaces which take their names from the js-sdk
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -243,7 +244,8 @@ export default function createRoom(opts: IOpts): Promise<string | null> {
|
||||||
|
|
||||||
// We also failed to join the room (this sets joining to false in RoomViewStore)
|
// We also failed to join the room (this sets joining to false in RoomViewStore)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'join_room_error',
|
action: Action.JoinRoomError,
|
||||||
|
roomId,
|
||||||
});
|
});
|
||||||
console.error("Failed to create room " + roomId + " " + err);
|
console.error("Failed to create room " + roomId + " " + err);
|
||||||
let description = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
let description = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||||
|
|
|
@ -139,6 +139,21 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
UploadCanceled = "upload_canceled",
|
UploadCanceled = "upload_canceled",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when requesting to join a room
|
||||||
|
*/
|
||||||
|
JoinRoom = "join_room",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when successfully joining a room
|
||||||
|
*/
|
||||||
|
JoinRoomReady = "join_room_ready",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when joining a room failed
|
||||||
|
*/
|
||||||
|
JoinRoomError = "join_room",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts content into the active composer. Should be used with ComposerInsertPayload
|
* Inserts content into the active composer. Should be used with ComposerInsertPayload
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -2753,6 +2753,8 @@
|
||||||
"Switch theme": "Switch theme",
|
"Switch theme": "Switch theme",
|
||||||
"User menu": "User menu",
|
"User menu": "User menu",
|
||||||
"Community and user menu": "Community and user menu",
|
"Community and user menu": "Community and user menu",
|
||||||
|
"Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms",
|
||||||
|
"Currently joining %(count)s rooms|one": "Currently joining %(count)s room",
|
||||||
"Could not load user profile": "Could not load user profile",
|
"Could not load user profile": "Could not load user profile",
|
||||||
"Decrypted event source": "Decrypted event source",
|
"Decrypted event source": "Decrypted event source",
|
||||||
"Original event source": "Original event source",
|
"Original event source": "Original event source",
|
||||||
|
|
|
@ -105,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) {
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode);
|
||||||
|
|
||||||
export interface IVariables {
|
export interface IVariables {
|
||||||
count?: number;
|
count?: number;
|
||||||
[key: string]: number | string;
|
[key: string]: SubstitutionValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tags = Record<string, (sub: string) => React.ReactNode>;
|
type Tags = Record<string, SubstitutionValue>;
|
||||||
|
|
||||||
export type TranslatedString = string | React.ReactNode;
|
export type TranslatedString = string | React.ReactNode;
|
||||||
|
|
||||||
|
@ -247,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
|
||||||
let replaced;
|
let replaced;
|
||||||
// If substitution is a function, call it
|
// If substitution is a function, call it
|
||||||
if (mapping[regexpString] instanceof Function) {
|
if (mapping[regexpString] instanceof Function) {
|
||||||
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups);
|
replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups);
|
||||||
} else {
|
} else {
|
||||||
replaced = mapping[regexpString];
|
replaced = mapping[regexpString];
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { _t } from '../languageHandler';
|
||||||
import dis from '../dispatcher/dispatcher';
|
import dis from '../dispatcher/dispatcher';
|
||||||
import { ISetting, SETTINGS } from "./Settings";
|
import { ISetting, SETTINGS } from "./Settings";
|
||||||
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
||||||
import { WatchManager } from "./WatchManager";
|
import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager";
|
||||||
import { SettingLevel } from "./SettingLevel";
|
import { SettingLevel } from "./SettingLevel";
|
||||||
import SettingsHandler from "./handlers/SettingsHandler";
|
import SettingsHandler from "./handlers/SettingsHandler";
|
||||||
|
|
||||||
|
@ -117,8 +117,8 @@ export default class SettingsStore {
|
||||||
// We also maintain a list of monitors which are special watchers: they cause dispatches
|
// We also maintain a list of monitors which are special watchers: they cause dispatches
|
||||||
// when the setting changes. We track which rooms we're monitoring though to ensure we
|
// when the setting changes. We track which rooms we're monitoring though to ensure we
|
||||||
// don't duplicate updates on the bus.
|
// don't duplicate updates on the bus.
|
||||||
private static watchers = {}; // { callbackRef => { callbackFn } }
|
private static watchers = new Map<string, WatchCallbackFn>();
|
||||||
private static monitors = {}; // { settingName => { roomId => callbackRef } }
|
private static monitors = new Map<string, Map<string, string>>(); // { settingName => { roomId => callbackRef } }
|
||||||
|
|
||||||
// Counter used for generation of watcher IDs
|
// Counter used for generation of watcher IDs
|
||||||
private static watcherCount = 1;
|
private static watcherCount = 1;
|
||||||
|
@ -163,7 +163,7 @@ export default class SettingsStore {
|
||||||
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingsStore.watchers[watcherId] = localizedCallback;
|
SettingsStore.watchers.set(watcherId, localizedCallback);
|
||||||
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
||||||
|
|
||||||
return watcherId;
|
return watcherId;
|
||||||
|
@ -176,13 +176,13 @@ export default class SettingsStore {
|
||||||
* to cancel.
|
* to cancel.
|
||||||
*/
|
*/
|
||||||
public static unwatchSetting(watcherReference: string) {
|
public static unwatchSetting(watcherReference: string) {
|
||||||
if (!SettingsStore.watchers[watcherReference]) {
|
if (!SettingsStore.watchers.has(watcherReference)) {
|
||||||
console.warn(`Ending non-existent watcher ID ${watcherReference}`);
|
console.warn(`Ending non-existent watcher ID ${watcherReference}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultWatchManager.unwatchSetting(SettingsStore.watchers[watcherReference]);
|
defaultWatchManager.unwatchSetting(SettingsStore.watchers.get(watcherReference));
|
||||||
delete SettingsStore.watchers[watcherReference];
|
SettingsStore.watchers.delete(watcherReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -196,10 +196,10 @@ export default class SettingsStore {
|
||||||
public static monitorSetting(settingName: string, roomId: string) {
|
public static monitorSetting(settingName: string, roomId: string) {
|
||||||
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
|
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
|
||||||
|
|
||||||
if (!this.monitors[settingName]) this.monitors[settingName] = {};
|
if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map());
|
||||||
|
|
||||||
const registerWatcher = () => {
|
const registerWatcher = () => {
|
||||||
this.monitors[settingName][roomId] = SettingsStore.watchSetting(
|
this.monitors.get(settingName).set(roomId, SettingsStore.watchSetting(
|
||||||
settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => {
|
settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'setting_updated',
|
action: 'setting_updated',
|
||||||
|
@ -210,19 +210,20 @@ export default class SettingsStore {
|
||||||
newValue,
|
newValue,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRoom = Object.keys(this.monitors[settingName]).find((r) => r === roomId || r === null);
|
const rooms = Array.from(this.monitors.get(settingName).keys());
|
||||||
|
const hasRoom = rooms.find((r) => r === roomId || r === null);
|
||||||
if (!hasRoom) {
|
if (!hasRoom) {
|
||||||
registerWatcher();
|
registerWatcher();
|
||||||
} else {
|
} else {
|
||||||
if (roomId === null) {
|
if (roomId === null) {
|
||||||
// Unregister all existing watchers and register the new one
|
// Unregister all existing watchers and register the new one
|
||||||
for (const roomId of Object.keys(this.monitors[settingName])) {
|
rooms.forEach(roomId => {
|
||||||
SettingsStore.unwatchSetting(this.monitors[settingName][roomId]);
|
SettingsStore.unwatchSetting(this.monitors.get(settingName).get(roomId));
|
||||||
}
|
});
|
||||||
this.monitors[settingName] = {};
|
this.monitors.get(settingName).clear();
|
||||||
registerWatcher();
|
registerWatcher();
|
||||||
} // else a watcher is already registered for the room, so don't bother registering it again
|
} // else a watcher is already registered for the room, so don't bother registering it again
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,7 @@ import { SettingLevel } from "./SettingLevel";
|
||||||
|
|
||||||
export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void;
|
export type CallbackFn = (changedInRoomId: string, atLevel: SettingLevel, newValAtLevel: any) => void;
|
||||||
|
|
||||||
const IRRELEVANT_ROOM: string = null;
|
const IRRELEVANT_ROOM = Symbol("irrelevant-room");
|
||||||
|
|
||||||
interface RoomWatcherMap {
|
|
||||||
[roomId: string]: CallbackFn[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
* Generalized management class for dealing with watchers on a per-handler (per-level)
|
||||||
|
@ -30,25 +26,25 @@ interface RoomWatcherMap {
|
||||||
* class, which are then proxied outwards to any applicable watchers.
|
* class, which are then proxied outwards to any applicable watchers.
|
||||||
*/
|
*/
|
||||||
export class WatchManager {
|
export class WatchManager {
|
||||||
private watchers: {[settingName: string]: RoomWatcherMap} = {};
|
private watchers = new Map<string, Map<string | symbol, CallbackFn[]>>(); // settingName -> roomId -> CallbackFn[]
|
||||||
|
|
||||||
// Proxy for handlers to delegate changes to this manager
|
// Proxy for handlers to delegate changes to this manager
|
||||||
public watchSetting(settingName: string, roomId: string | null, cb: CallbackFn) {
|
public watchSetting(settingName: string, roomId: string | null, cb: CallbackFn) {
|
||||||
if (!this.watchers[settingName]) this.watchers[settingName] = {};
|
if (!this.watchers.has(settingName)) this.watchers.set(settingName, new Map());
|
||||||
if (!this.watchers[settingName][roomId]) this.watchers[settingName][roomId] = [];
|
if (!this.watchers.get(settingName).has(roomId)) this.watchers.get(settingName).set(roomId, []);
|
||||||
this.watchers[settingName][roomId].push(cb);
|
this.watchers.get(settingName).get(roomId).push(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy for handlers to delegate changes to this manager
|
// Proxy for handlers to delegate changes to this manager
|
||||||
public unwatchSetting(cb: CallbackFn) {
|
public unwatchSetting(cb: CallbackFn) {
|
||||||
for (const settingName of Object.keys(this.watchers)) {
|
this.watchers.forEach((map) => {
|
||||||
for (const roomId of Object.keys(this.watchers[settingName])) {
|
map.forEach((callbacks) => {
|
||||||
let idx;
|
let idx;
|
||||||
while ((idx = this.watchers[settingName][roomId].indexOf(cb)) !== -1) {
|
while ((idx = callbacks.indexOf(cb)) !== -1) {
|
||||||
this.watchers[settingName][roomId].splice(idx, 1);
|
callbacks.splice(idx, 1);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public notifyUpdate(settingName: string, inRoomId: string | null, atLevel: SettingLevel, newValueAtLevel: any) {
|
public notifyUpdate(settingName: string, inRoomId: string | null, atLevel: SettingLevel, newValueAtLevel: any) {
|
||||||
|
@ -56,21 +52,21 @@ export class WatchManager {
|
||||||
// we also don't have a reliable way to get the old value of a setting. Instead, we'll just
|
// we also don't have a reliable way to get the old value of a setting. Instead, we'll just
|
||||||
// let it fall through regardless and let the receiver dedupe if they want to.
|
// let it fall through regardless and let the receiver dedupe if they want to.
|
||||||
|
|
||||||
if (!this.watchers[settingName]) return;
|
if (!this.watchers.has(settingName)) return;
|
||||||
|
|
||||||
const roomWatchers = this.watchers[settingName];
|
const roomWatchers = this.watchers.get(settingName);
|
||||||
const callbacks = [];
|
const callbacks = [];
|
||||||
|
|
||||||
if (inRoomId !== null && roomWatchers[inRoomId]) {
|
if (inRoomId !== null && roomWatchers.has(inRoomId)) {
|
||||||
callbacks.push(...roomWatchers[inRoomId]);
|
callbacks.push(...roomWatchers.get(inRoomId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inRoomId) {
|
if (!inRoomId) {
|
||||||
// Fire updates to all the individual room watchers too, as they probably
|
// Fire updates to all the individual room watchers too, as they probably care about the change higher up.
|
||||||
// care about the change higher up.
|
const callbacks = Array.from(roomWatchers.values()).flat(1);
|
||||||
callbacks.push(...Object.values(roomWatchers).flat(1));
|
callbacks.push(...callbacks);
|
||||||
} else if (roomWatchers[IRRELEVANT_ROOM]) {
|
} else if (roomWatchers.has(IRRELEVANT_ROOM)) {
|
||||||
callbacks.push(...roomWatchers[IRRELEVANT_ROOM]);
|
callbacks.push(...roomWatchers.get(IRRELEVANT_ROOM));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const callback of callbacks) {
|
for (const callback of callbacks) {
|
||||||
|
|
|
@ -17,17 +17,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Store} from 'flux/utils';
|
import { Store } from 'flux/utils';
|
||||||
import {MatrixError} from "matrix-js-sdk/src/http-api";
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
|
||||||
import dis from '../dispatcher/dispatcher';
|
import dis from '../dispatcher/dispatcher';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import Modal from '../Modal';
|
import Modal from '../Modal';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
|
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from '../RoomAliasCache';
|
||||||
import {ActionPayload} from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
import {retry} from "../utils/promise";
|
import { Action } from "../dispatcher/actions";
|
||||||
|
import { retry } from "../utils/promise";
|
||||||
import CountlyAnalytics from "../CountlyAnalytics";
|
import CountlyAnalytics from "../CountlyAnalytics";
|
||||||
|
|
||||||
const NUM_JOIN_RETRY = 5;
|
const NUM_JOIN_RETRY = 5;
|
||||||
|
@ -136,13 +137,13 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
break;
|
break;
|
||||||
// join_room:
|
// join_room:
|
||||||
// - opts: options for joinRoom
|
// - opts: options for joinRoom
|
||||||
case 'join_room':
|
case Action.JoinRoom:
|
||||||
this.joinRoom(payload);
|
this.joinRoom(payload);
|
||||||
break;
|
break;
|
||||||
case 'join_room_error':
|
case Action.JoinRoomError:
|
||||||
this.joinRoomError(payload);
|
this.joinRoomError(payload);
|
||||||
break;
|
break;
|
||||||
case 'join_room_ready':
|
case Action.JoinRoomReady:
|
||||||
this.setState({ shouldPeek: false });
|
this.setState({ shouldPeek: false });
|
||||||
break;
|
break;
|
||||||
case 'on_client_not_viable':
|
case 'on_client_not_viable':
|
||||||
|
@ -217,7 +218,11 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
|
|
||||||
if (payload.auto_join) {
|
if (payload.auto_join) {
|
||||||
this.joinRoom(payload);
|
dis.dispatch({
|
||||||
|
...payload,
|
||||||
|
action: Action.JoinRoom,
|
||||||
|
roomId: payload.room_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (payload.room_alias) {
|
} else if (payload.room_alias) {
|
||||||
// Try the room alias to room ID navigation cache first to avoid
|
// Try the room alias to room ID navigation cache first to avoid
|
||||||
|
@ -298,41 +303,16 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
|
// We do *not* clear the 'joining' flag because the Room object and/or our 'joined' member event may not
|
||||||
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
|
// have come down the sync stream yet, and that's the point at which we'd consider the user joined to the
|
||||||
// room.
|
// room.
|
||||||
dis.dispatch({ action: 'join_room_ready' });
|
dis.dispatch({
|
||||||
|
action: Action.JoinRoomReady,
|
||||||
|
roomId: this.state.roomId,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'join_room_error',
|
action: Action.JoinRoomError,
|
||||||
|
roomId: this.state.roomId,
|
||||||
err: err,
|
err: err,
|
||||||
});
|
});
|
||||||
|
|
||||||
let msg = err.message ? err.message : JSON.stringify(err);
|
|
||||||
console.log("Failed to join room:", msg);
|
|
||||||
|
|
||||||
if (err.name === "ConnectionError") {
|
|
||||||
msg = _t("There was an error joining the room");
|
|
||||||
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
|
||||||
msg = <div>
|
|
||||||
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
|
|
||||||
{_t("Please contact your homeserver administrator.")}
|
|
||||||
</div>;
|
|
||||||
} else if (err.httpStatus === 404) {
|
|
||||||
const invitingUserId = this.getInvitingUserId(this.state.roomId);
|
|
||||||
// only provide a better error message for invites
|
|
||||||
if (invitingUserId) {
|
|
||||||
// if the inviting user is on the same HS, there can only be one cause: they left.
|
|
||||||
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
|
|
||||||
msg = _t("The person who invited you already left the room.");
|
|
||||||
} else {
|
|
||||||
msg = _t("The person who invited you already left the room, or their server is offline.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
|
||||||
title: _t("Failed to join room"),
|
|
||||||
description: msg,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -351,6 +331,35 @@ class RoomViewStore extends Store<ActionPayload> {
|
||||||
joining: false,
|
joining: false,
|
||||||
joinError: payload.err,
|
joinError: payload.err,
|
||||||
});
|
});
|
||||||
|
const err = payload.err;
|
||||||
|
let msg = err.message ? err.message : JSON.stringify(err);
|
||||||
|
console.log("Failed to join room:", msg);
|
||||||
|
|
||||||
|
if (err.name === "ConnectionError") {
|
||||||
|
msg = _t("There was an error joining the room");
|
||||||
|
} else if (err.errcode === 'M_INCOMPATIBLE_ROOM_VERSION') {
|
||||||
|
msg = <div>
|
||||||
|
{_t("Sorry, your homeserver is too old to participate in this room.")}<br />
|
||||||
|
{_t("Please contact your homeserver administrator.")}
|
||||||
|
</div>;
|
||||||
|
} else if (err.httpStatus === 404) {
|
||||||
|
const invitingUserId = this.getInvitingUserId(this.state.roomId);
|
||||||
|
// only provide a better error message for invites
|
||||||
|
if (invitingUserId) {
|
||||||
|
// if the inviting user is on the same HS, there can only be one cause: they left.
|
||||||
|
if (invitingUserId.endsWith(`:${MatrixClientPeg.get().getDomain()}`)) {
|
||||||
|
msg = _t("The person who invited you already left the room.");
|
||||||
|
} else {
|
||||||
|
msg = _t("The person who invited you already left the room, or their server is offline.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, {
|
||||||
|
title: _t("Failed to join room"),
|
||||||
|
description: msg,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset() {
|
public reset() {
|
||||||
|
|
73
src/stores/UIStore.ts
Normal file
73
src/stores/UIStore.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
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 EventEmitter from "events";
|
||||||
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
|
import ResizeObserverEntry from 'resize-observer-polyfill/src/ResizeObserverEntry';
|
||||||
|
|
||||||
|
export enum UI_EVENTS {
|
||||||
|
Resize = "resize"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
||||||
|
|
||||||
|
|
||||||
|
export default class UIStore extends EventEmitter {
|
||||||
|
private static _instance: UIStore = null;
|
||||||
|
|
||||||
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
public windowWidth: number;
|
||||||
|
public windowHeight: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
this.windowWidth = window.innerWidth;
|
||||||
|
// eslint-disable-next-line no-restricted-properties
|
||||||
|
this.windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(this.resizeObserverCallback);
|
||||||
|
this.resizeObserver.observe(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): UIStore {
|
||||||
|
if (!UIStore._instance) {
|
||||||
|
UIStore._instance = new UIStore();
|
||||||
|
}
|
||||||
|
return UIStore._instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static destroy(): void {
|
||||||
|
if (UIStore._instance) {
|
||||||
|
UIStore._instance.resizeObserver.disconnect();
|
||||||
|
UIStore._instance.removeAllListeners();
|
||||||
|
UIStore._instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
||||||
|
const { width, height } = entries
|
||||||
|
.find(entry => entry.target === document.body)
|
||||||
|
.contentRect;
|
||||||
|
|
||||||
|
this.windowWidth = width;
|
||||||
|
this.windowHeight = height;
|
||||||
|
|
||||||
|
this.emit(UI_EVENTS.Resize, entries);
|
||||||
|
}
|
||||||
|
}
|
|
@ -176,7 +176,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||||
const event = payload.event; // TODO: Type out the dispatcher
|
const event = payload.event; // TODO: Type out the dispatcher
|
||||||
if (!this.previews.has(event.getRoomId())) return; // not important
|
const isHistoricalEvent = payload.hasOwnProperty("isLiveEvent") && !payload.isLiveEvent
|
||||||
|
if (!this.previews.has(event.getRoomId()) || isHistoricalEvent) return; // not important
|
||||||
await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
await this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,12 +74,6 @@ export default class ResizeNotifier extends EventEmitter {
|
||||||
|
|
||||||
// can be called in quick succession
|
// can be called in quick succession
|
||||||
notifyWindowResized() {
|
notifyWindowResized() {
|
||||||
// no need to throttle this one,
|
|
||||||
// also it could make scrollbars appear for
|
|
||||||
// a split second when the room list manual layout is now
|
|
||||||
// taller than the available space
|
|
||||||
this.emit("leftPanelResized");
|
|
||||||
|
|
||||||
this._updateMiddlePanel();
|
this._updateMiddlePanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,24 +51,6 @@ export function defer<T>(): IDeferred<T> {
|
||||||
return {resolve, reject, promise};
|
return {resolve, reject, promise};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Promise.allSettled polyfill until browser support is stable in Firefox
|
|
||||||
export function allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>> {
|
|
||||||
if (Promise.allSettled) {
|
|
||||||
return Promise.allSettled<T>(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore - typescript isn't smart enough to see the disjoint here
|
|
||||||
return Promise.all(promises.map((promise) => {
|
|
||||||
return promise.then(value => ({
|
|
||||||
status: "fulfilled",
|
|
||||||
value,
|
|
||||||
})).catch(reason => ({
|
|
||||||
status: "rejected",
|
|
||||||
reason,
|
|
||||||
}));
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper method to retry a Promise a given number of times or until a predicate fails
|
// Helper method to retry a Promise a given number of times or until a predicate fails
|
||||||
export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
|
export async function retry<T, E extends Error>(fn: () => Promise<T>, num: number, predicate?: (e: E) => boolean) {
|
||||||
let lastErr: E;
|
let lastErr: E;
|
||||||
|
|
Loading…
Reference in a new issue