Merge remote-tracking branch 'upstream/develop' into SuppressSpeechWhenSending
This commit is contained in:
commit
d8373576f8
1188 changed files with 53232 additions and 22354 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,93 +17,10 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
|
||||
// derived from code from github.com/noeldelgado/gemini-scrollbar
|
||||
// Copyright (c) Noel Delgado <pixelia.me@gmail.com> (pixelia.me)
|
||||
function getScrollbarWidth(alternativeOverflow) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = '-9999px';
|
||||
div.style.width = '100px';
|
||||
div.style.height = '100px';
|
||||
div.style.overflow = "scroll";
|
||||
if (alternativeOverflow) {
|
||||
div.style.overflow = alternativeOverflow;
|
||||
}
|
||||
div.style.msOverflowStyle = '-ms-autohiding-scrollbar';
|
||||
document.body.appendChild(div);
|
||||
const scrollbarWidth = (div.offsetWidth - div.clientWidth);
|
||||
document.body.removeChild(div);
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
function install() {
|
||||
const scrollbarWidth = getScrollbarWidth();
|
||||
if (scrollbarWidth !== 0) {
|
||||
const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0;
|
||||
// overflow: overlay on webkit doesn't auto hide the scrollbar
|
||||
if (hasForcedOverlayScrollbar) {
|
||||
document.body.classList.add("mx_scrollbar_overlay_noautohide");
|
||||
} else {
|
||||
document.body.classList.add("mx_scrollbar_nooverlay");
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.innerText =
|
||||
`body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const installBodyClassesIfNeeded = (function() {
|
||||
let installed = false;
|
||||
return function() {
|
||||
if (!installed) {
|
||||
install();
|
||||
installed = true;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
export default class AutoHideScrollbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onOverflow = this.onOverflow.bind(this);
|
||||
this.onUnderflow = this.onUnderflow.bind(this);
|
||||
this._collectContainerRef = this._collectContainerRef.bind(this);
|
||||
this._needsOverflowListener = null;
|
||||
}
|
||||
|
||||
onOverflow() {
|
||||
this.containerRef.classList.add("mx_AutoHideScrollbar_overflow");
|
||||
this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow");
|
||||
}
|
||||
|
||||
onUnderflow() {
|
||||
this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow");
|
||||
this.containerRef.classList.add("mx_AutoHideScrollbar_underflow");
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
if (!this._needsOverflowListener) {
|
||||
return;
|
||||
}
|
||||
if (this.containerRef.scrollHeight > this.containerRef.clientHeight) {
|
||||
this.onOverflow();
|
||||
} else {
|
||||
this.onUnderflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
installBodyClassesIfNeeded();
|
||||
this._needsOverflowListener =
|
||||
document.body.classList.contains("mx_scrollbar_nooverlay");
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
_collectContainerRef(ref) {
|
||||
|
@ -126,9 +44,7 @@ export default class AutoHideScrollbar extends React.Component {
|
|||
onScroll={this.props.onScroll}
|
||||
onWheel={this.props.onWheel}
|
||||
>
|
||||
<div className="mx_AutoHideScrollbar_offset">
|
||||
{ this.props.children }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,6 +20,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CompatibilityPage',
|
||||
|
@ -38,14 +39,25 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
return (
|
||||
<div className="mx_CompatibilityPage">
|
||||
<div className="mx_CompatibilityPage_box">
|
||||
<p>{ _t("Sorry, your browser is <b>not</b> able to run Riot.", {}, { 'b': (sub) => <b>{sub}</b> }) } </p>
|
||||
<p>{_t(
|
||||
"Sorry, your browser is <b>not</b> able to run %(brand)s.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
{
|
||||
'b': (sub) => <b>{sub}</b>,
|
||||
})
|
||||
}</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"Riot uses many advanced browser features, some of which are not available " +
|
||||
"%(brand)s uses many advanced browser features, some of which are not available " +
|
||||
"or experimental in your current browser.",
|
||||
{ brand },
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
|
|
|
@ -16,13 +16,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useRef, useState} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React, {CSSProperties, useRef, useState} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
|
||||
import {Key} from "../../Keyboard";
|
||||
import * as sdk from "../../index";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import {Writeable} from "../../@types/common";
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -30,8 +29,8 @@ import AccessibleButton from "../views/elements/AccessibleButton";
|
|||
|
||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContextualMenuContainerId);
|
||||
function getOrCreateContainer(): HTMLDivElement {
|
||||
let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement;
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
|
@ -43,50 +42,70 @@ function getOrCreateContainer() {
|
|||
}
|
||||
|
||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||
|
||||
interface IPosition {
|
||||
top?: number;
|
||||
bottom?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
}
|
||||
|
||||
export enum ChevronFace {
|
||||
Top = "top",
|
||||
Bottom = "bottom",
|
||||
Left = "left",
|
||||
Right = "right",
|
||||
None = "none",
|
||||
}
|
||||
|
||||
interface IProps extends IPosition {
|
||||
menuWidth?: number;
|
||||
menuHeight?: number;
|
||||
|
||||
chevronOffset?: number;
|
||||
chevronFace?: ChevronFace;
|
||||
|
||||
menuPaddingTop?: number;
|
||||
menuPaddingBottom?: number;
|
||||
menuPaddingLeft?: number;
|
||||
menuPaddingRight?: number;
|
||||
|
||||
zIndex?: number;
|
||||
|
||||
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
|
||||
hasBackground?: boolean;
|
||||
// whether this context menu should be focus managed. If false it must handle itself
|
||||
managed?: boolean;
|
||||
|
||||
// Function to be called on menu close
|
||||
onFinished();
|
||||
// on resize callback
|
||||
windowResize?();
|
||||
}
|
||||
|
||||
interface IState {
|
||||
contextMenuElem: HTMLDivElement;
|
||||
}
|
||||
|
||||
// Generic ContextMenu Portal wrapper
|
||||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||
export class ContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
top: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
||||
// Function to be called on menu close
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
menuPaddingTop: PropTypes.number,
|
||||
menuPaddingRight: PropTypes.number,
|
||||
menuPaddingBottom: PropTypes.number,
|
||||
menuPaddingLeft: PropTypes.number,
|
||||
zIndex: PropTypes.number,
|
||||
|
||||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
|
||||
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
|
||||
};
|
||||
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||
private initialFocus: HTMLElement;
|
||||
|
||||
static defaultProps = {
|
||||
hasBackground: true,
|
||||
managed: true,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
contextMenuElem: null,
|
||||
};
|
||||
|
||||
// persist what had focus when we got initialized so we can return it after
|
||||
this.initialFocus = document.activeElement;
|
||||
this.initialFocus = document.activeElement as HTMLElement;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -94,7 +113,7 @@ export class ContextMenu extends React.Component {
|
|||
this.initialFocus.focus();
|
||||
}
|
||||
|
||||
collectContextMenuRect = (element) => {
|
||||
private collectContextMenuRect = (element) => {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
|
@ -111,11 +130,12 @@ export class ContextMenu extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onContextMenu = (e) => {
|
||||
private onContextMenu = (e) => {
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
|
@ -133,7 +153,20 @@ export class ContextMenu extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_onMoveFocus = (element, up) => {
|
||||
private onContextMenuPreventBubbling = (e) => {
|
||||
// stop propagation so that any context menu handlers don't leak out of this context menu
|
||||
// but do not inhibit the default browser menu
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// Prevent clicks on the background from going through to the component which opened the menu.
|
||||
private onFinished = (ev: React.MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (this.props.onFinished) this.props.onFinished();
|
||||
};
|
||||
|
||||
private onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
|
||||
do {
|
||||
|
@ -167,25 +200,25 @@ export class ContextMenu extends React.Component {
|
|||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onMoveFocusHomeEnd = (element, up) => {
|
||||
private onMoveFocusHomeEnd = (element: Element, up: boolean) => {
|
||||
let results = element.querySelectorAll('[role^="menuitem"]');
|
||||
if (!results) {
|
||||
results = element.querySelectorAll('[tab-index]');
|
||||
}
|
||||
if (results && results.length) {
|
||||
if (up) {
|
||||
results[0].focus();
|
||||
(results[0] as HTMLElement).focus();
|
||||
} else {
|
||||
results[results.length - 1].focus();
|
||||
(results[results.length - 1] as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (!this.props.managed) {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
this.props.onFinished();
|
||||
|
@ -203,16 +236,16 @@ export class ContextMenu extends React.Component {
|
|||
this.props.onFinished();
|
||||
break;
|
||||
case Key.ARROW_UP:
|
||||
this._onMoveFocus(ev.target, true);
|
||||
this.onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
this._onMoveFocus(ev.target, false);
|
||||
this.onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
case Key.HOME:
|
||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||
this.onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||
break;
|
||||
case Key.END:
|
||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||
this.onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
|
@ -225,9 +258,8 @@ export class ContextMenu extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
renderMenu(hasBackground=this.props.hasBackground) {
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
protected renderMenu(hasBackground = this.props.hasBackground) {
|
||||
const position: Partial<Writeable<DOMRect>> = {};
|
||||
const props = this.props;
|
||||
|
||||
if (props.top) {
|
||||
|
@ -236,24 +268,24 @@ export class ContextMenu extends React.Component {
|
|||
position.bottom = props.bottom;
|
||||
}
|
||||
|
||||
let chevronFace: ChevronFace;
|
||||
if (props.left) {
|
||||
position.left = props.left;
|
||||
chevronFace = 'left';
|
||||
chevronFace = ChevronFace.Left;
|
||||
} else {
|
||||
position.right = props.right;
|
||||
chevronFace = 'right';
|
||||
chevronFace = ChevronFace.Right;
|
||||
}
|
||||
|
||||
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
||||
const padding = 10;
|
||||
|
||||
const chevronOffset = {};
|
||||
const chevronOffset: CSSProperties = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
const hasChevron = chevronFace && chevronFace !== "none";
|
||||
const hasChevron = chevronFace && chevronFace !== ChevronFace.None;
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else if (position.top !== undefined) {
|
||||
const target = position.top;
|
||||
|
@ -264,7 +296,8 @@ export class ContextMenu extends React.Component {
|
|||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
const padding = 10;
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding);
|
||||
}
|
||||
|
||||
position.top = adjusted;
|
||||
|
@ -282,13 +315,13 @@ export class ContextMenu extends React.Component {
|
|||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
||||
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
||||
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||
});
|
||||
|
||||
const menuStyle = {};
|
||||
const menuStyle: CSSProperties = {};
|
||||
if (props.menuWidth) {
|
||||
menuStyle.width = props.menuWidth;
|
||||
}
|
||||
|
@ -319,13 +352,28 @@ export class ContextMenu extends React.Component {
|
|||
let background;
|
||||
if (hasBackground) {
|
||||
background = (
|
||||
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={props.onFinished} onContextMenu={this.onContextMenu} />
|
||||
<div
|
||||
className="mx_ContextualMenu_background"
|
||||
style={wrapperStyle}
|
||||
onClick={this.onFinished}
|
||||
onContextMenu={this.onContextMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
|
||||
<div
|
||||
className="mx_ContextualMenu_wrapper"
|
||||
style={{...position, ...wrapperStyle}}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={this.props.managed ? "menu" : undefined}
|
||||
>
|
||||
{ chevron }
|
||||
{ props.children }
|
||||
</div>
|
||||
|
@ -334,92 +382,13 @@ export class ContextMenu extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): React.ReactChild {
|
||||
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
||||
}
|
||||
}
|
||||
|
||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||
export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} title={label} aria-label={label} aria-haspopup={true} aria-expanded={isExpanded}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
ContextMenuButton.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitem
|
||||
export const MenuItem = ({children, label, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItem.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
||||
export const MenuGroup = ({children, label, ...props}) => {
|
||||
return <div {...props} role="group" aria-label={label}>
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
MenuGroup.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
className: PropTypes.string, // optional
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitemcheckbox
|
||||
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItemCheckbox.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
active: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Semantic component for representing a role=menuitemradio
|
||||
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
MenuItemRadio.propTypes = {
|
||||
...AccessibleButton.propTypes,
|
||||
label: PropTypes.string, // optional
|
||||
active: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool, // optional
|
||||
className: PropTypes.string, // optional
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||
export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||
const left = elementRect.right + window.pageXOffset + 3;
|
||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||
|
@ -427,8 +396,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
|
|||
};
|
||||
|
||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||
export const aboveLeftOf = (elementRect, chevronFace="none") => {
|
||||
const menuOptions = { chevronFace };
|
||||
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
|
||||
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||
|
||||
const buttonRight = elementRect.right + window.pageXOffset;
|
||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||
|
@ -486,3 +455,12 @@ export function createMenu(ElementClass, props) {
|
|||
|
||||
return {close: onFinished};
|
||||
}
|
||||
|
||||
// re-export the semantic helper components for simplicity
|
||||
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
|
||||
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
|
||||
export {MenuItem} from "../../accessibility/context_menu/MenuItem";
|
||||
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";
|
||||
export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio";
|
||||
export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox";
|
||||
export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio";
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
|
@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
|
||||
this.setState({tags: CustomRoomTagStore.getSortedTags()});
|
||||
});
|
||||
|
|
|
@ -23,11 +23,11 @@ import PropTypes from 'prop-types';
|
|||
import request from 'browser-request';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default class EmbeddedPage extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
className: PropTypes.string,
|
||||
// Whether to wrap the page in a scrollbar
|
||||
scrollbar: PropTypes.bool,
|
||||
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
||||
replaceMap: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -56,7 +58,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
return sanitizeHtml(_t(s));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
|
||||
if (!this.props.url) {
|
||||
|
@ -81,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1));
|
||||
|
||||
if (this.props.replaceMap) {
|
||||
Object.keys(this.props.replaceMap).forEach(key => {
|
||||
body = body.split(key).join(this.props.replaceMap[key]);
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ page: body });
|
||||
},
|
||||
);
|
||||
|
@ -117,10 +126,9 @@ export default class EmbeddedPage extends React.PureComponent {
|
|||
</div>;
|
||||
|
||||
if (this.props.scrollbar) {
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
return <GeminiScrollbarWrapper autoshow={true} className={classes}>
|
||||
return <AutoHideScrollbar className={classes}>
|
||||
{content}
|
||||
</GeminiScrollbarWrapper>;
|
||||
</AutoHideScrollbar>;
|
||||
} else {
|
||||
return <div className={classes}>
|
||||
{content}
|
||||
|
|
|
@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { getHostingLink } from '../../utils/HostingLink';
|
||||
import { sanitizedHtmlNode } from '../../HtmlUtils';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
|
@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm
|
|||
import {Group} from "matrix-js-sdk";
|
||||
import {allSettled, sleep} from "../../utils/promise";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
|
@ -91,7 +92,7 @@ const CategoryRoomList = createReactClass({
|
|||
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add rooms to the community summary'),
|
||||
description: _t("Which rooms would you like to add to this summary?"),
|
||||
placeholder: _t("Room name or alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to summary"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
@ -423,28 +424,35 @@ export default createReactClass({
|
|||
membershipBusy: false,
|
||||
publicityBusy: false,
|
||||
inviterProfile: null,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
|
||||
this._changeAvatarComponent = null;
|
||||
this._initGroupStore(this.props.groupId, true);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
|
||||
// Remove RightPanelStore listener
|
||||
if (this._rightPanelStoreToken) {
|
||||
this._rightPanelStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (this.props.groupId != newProps.groupId) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
if (this.props.groupId !== newProps.groupId) {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: null,
|
||||
|
@ -454,6 +462,12 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup,
|
||||
});
|
||||
},
|
||||
|
||||
_onGroupMyMembership: function(group) {
|
||||
if (this._unmounted || group.groupId !== this.props.groupId) return;
|
||||
if (group.myMembership === 'leave') {
|
||||
|
@ -481,7 +495,7 @@ export default createReactClass({
|
|||
group_id: groupId,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}});
|
||||
willDoOnboarding = true;
|
||||
}
|
||||
if (stateKey === GroupStore.STATE_KEY.Summary) {
|
||||
|
@ -554,10 +568,6 @@ export default createReactClass({
|
|||
GROUP_JOINPOLICY_INVITE,
|
||||
},
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
sideDisabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
_onShareClick: function() {
|
||||
|
@ -580,10 +590,6 @@ export default createReactClass({
|
|||
profileForm: null,
|
||||
});
|
||||
break;
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -726,7 +732,7 @@ export default createReactClass({
|
|||
|
||||
_onJoinClick: async function() {
|
||||
if (this._matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1173,7 +1179,6 @@ export default createReactClass({
|
|||
render: function() {
|
||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
|
||||
return <Spinner />;
|
||||
|
@ -1299,9 +1304,7 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup
|
||||
? <RightPanel groupId={this.props.groupId} />
|
||||
: undefined;
|
||||
const rightPanel = this.state.showRightPanel ? <RightPanel groupId={this.props.groupId} /> : undefined;
|
||||
|
||||
const headerClasses = {
|
||||
"mx_GroupView_header": true,
|
||||
|
@ -1332,10 +1335,10 @@ export default createReactClass({
|
|||
<GroupHeaderButtons />
|
||||
</div>
|
||||
<MainSplit panel={rightPanel}>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
<AutoHideScrollbar className="mx_GroupView_body">
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</MainSplit>
|
||||
</main>
|
||||
);
|
||||
|
|
67
src/components/structures/HomePage.tsx
Normal file
67
src/components/structures/HomePage.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { getHomePageUrl } from "../../utils/pages";
|
||||
import { _t } from "../../languageHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'});
|
||||
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
||||
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
|
||||
|
||||
const HomePage = () => {
|
||||
const config = SdkConfig.get();
|
||||
const pageUrl = getHomePageUrl(config);
|
||||
|
||||
if (pageUrl) {
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
|
||||
}
|
||||
|
||||
const brandingConfig = config.branding;
|
||||
let logoUrl = "themes/element/img/logos/element-logo.svg";
|
||||
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
|
||||
logoUrl = brandingConfig.authHeaderLogoUrl;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
<img src={logoUrl} alt={config.brand || "Riot"} />
|
||||
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }</h1>
|
||||
<h4>{ _t("Liberate your communication") }</h4>
|
||||
<div className="mx_HomePage_default_buttons">
|
||||
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
|
||||
{ _t("Send a Direct Message") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onClickExplore} className="mx_HomePage_button_explore">
|
||||
{ _t("Explore Public Rooms") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onClickNewRoom} className="mx_HomePage_button_createGroup">
|
||||
{ _t("Create a Group Chat") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</AutoHideScrollbar>;
|
||||
};
|
||||
|
||||
export default HomePage;
|
|
@ -66,6 +66,22 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this._autoHideScrollbar = autoHideScrollbar;
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
|
||||
const curLen = this.props.children && this.props.children.length || 0;
|
||||
// check overflow only if amount of children changes.
|
||||
// if we don't guard here, we end up with an infinite
|
||||
// render > componentDidUpdate > checkOverflow > setState > render loop
|
||||
if (prevLen !== curLen) {
|
||||
this.checkOverflow();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkOverflow();
|
||||
}
|
||||
|
||||
checkOverflow() {
|
||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
||||
|
@ -95,10 +111,6 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
||||
}
|
||||
|
||||
if (this._autoHideScrollbar) {
|
||||
this._autoHideScrollbar.checkOverflow();
|
||||
}
|
||||
|
||||
if (this.props.trackHorizontalOverflow) {
|
||||
this.setState({
|
||||
// Offset from absolute position of the container
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom
|
|||
|
||||
import * as sdk from '../../index';
|
||||
|
||||
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InteractiveAuth',
|
||||
|
||||
|
@ -47,7 +49,7 @@ export default createReactClass({
|
|||
// @param {bool} status True if the operation requiring
|
||||
// auth was completed sucessfully, false if canceled.
|
||||
// @param {object} result The result of the authenticated call
|
||||
// if successful, otherwise the error object
|
||||
// if successful, otherwise the error object.
|
||||
// @param {object} extra Additional information about the UI Auth
|
||||
// process:
|
||||
// * emailSid {string} If email auth was performed, the sid of
|
||||
|
@ -75,6 +77,15 @@ export default createReactClass({
|
|||
// is managed by some other party and should not be managed by
|
||||
// the component itself.
|
||||
continueIsManaged: PropTypes.bool,
|
||||
|
||||
// Called when the stage changes, or the stage's phase changes. First
|
||||
// argument is the stage, second is the phase. Some stages do not have
|
||||
// phases and will be counted as 0 (numeric).
|
||||
onStagePhaseChange: PropTypes.func,
|
||||
|
||||
// continueText and continueKind are passed straight through to the AuthEntryComponent.
|
||||
continueText: PropTypes.string,
|
||||
continueKind: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -87,7 +98,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
|
@ -161,6 +173,7 @@ export default createReactClass({
|
|||
_authStateUpdated: function(stageType, stageState) {
|
||||
const oldStage = this.state.authStage;
|
||||
this.setState({
|
||||
busy: false,
|
||||
authStage: stageType,
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
|
@ -184,11 +197,13 @@ export default createReactClass({
|
|||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
// immediate work has completed, but that's not really what we want at
|
||||
// the UI layer, so we ignore this signal and show a spinner until
|
||||
// there's a new screen to show the user. This is implemented by setting
|
||||
// `busy: false` in `_authStateUpdated`.
|
||||
// See also https://github.com/vector-im/riot-web/issues/12546
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
|
@ -201,6 +216,16 @@ export default createReactClass({
|
|||
this._authLogic.submitAuthDict(authData);
|
||||
},
|
||||
|
||||
_onPhaseChange: function(newPhase) {
|
||||
if (this.props.onStagePhaseChange) {
|
||||
this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
|
||||
}
|
||||
},
|
||||
|
||||
_onStageCancel: function() {
|
||||
this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
|
||||
},
|
||||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
if (!stage) {
|
||||
|
@ -229,6 +254,10 @@ export default createReactClass({
|
|||
fail={this._onAuthStageFailed}
|
||||
setEmailSid={this._setEmailSid}
|
||||
showContinue={!this.props.continueIsManaged}
|
||||
onPhaseChange={this._onPhaseChange}
|
||||
continueText={this.props.continueText}
|
||||
continueKind={this.props.continueKind}
|
||||
onCancel={this._onStageCancel}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -21,11 +21,12 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { Key } from '../../Keyboard';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import Analytics from "../../Analytics";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
|
||||
const LeftPanel = createReactClass({
|
||||
|
@ -44,7 +45,8 @@ const LeftPanel = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this.focusedElement = null;
|
||||
|
||||
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
|
||||
|
@ -196,7 +198,7 @@ const LeftPanel = createReactClass({
|
|||
|
||||
onSearchCleared: function(source) {
|
||||
if (source === "keyboard") {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
this.setState({searchExpanded: false});
|
||||
},
|
||||
|
@ -250,7 +252,7 @@ const LeftPanel = createReactClass({
|
|||
if (!this.props.collapsed) {
|
||||
exploreButton = (
|
||||
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
|
||||
<AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton>
|
||||
<AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -272,6 +274,16 @@ const LeftPanel = createReactClass({
|
|||
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
const roomList = <RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />;
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{ tagPanelContainer }
|
||||
|
@ -283,15 +295,7 @@ const LeftPanel = createReactClass({
|
|||
{ exploreButton }
|
||||
{ searchBox }
|
||||
</div>
|
||||
<RoomList
|
||||
onKeyDown={this._onKeyDown}
|
||||
onFocus={this._onFocus}
|
||||
onBlur={this._onBlur}
|
||||
ref={this.collectRoomList}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapsed}
|
||||
searchFilter={this.state.searchFilter}
|
||||
ConferenceHandler={VectorConferenceHandler} />
|
||||
{roomList}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
411
src/components/structures/LeftPanel2.tsx
Normal file
411
src/components/structures/LeftPanel2.tsx
Normal file
|
@ -0,0 +1,411 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import TagPanel from "./TagPanel";
|
||||
import classNames from "classnames";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import RoomList2 from "../views/rooms/RoomList2";
|
||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import UserMenu from "./UserMenu";
|
||||
import RoomSearch from "./RoomSearch";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
|
||||
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
||||
import {Key} from "../../Keyboard";
|
||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||
|
||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
searchFilter: string;
|
||||
showBreadcrumbs: boolean;
|
||||
showTagPanel: boolean;
|
||||
}
|
||||
|
||||
// List of CSS classes which should be included in keyboard navigation within the room list
|
||||
const cssClasses = [
|
||||
"mx_RoomSearch_input",
|
||||
"mx_RoomSearch_icon", // minimized <RoomSearch />
|
||||
"mx_RoomSublist2_headerText",
|
||||
"mx_RoomTile2",
|
||||
"mx_RoomSublist2_showNButton",
|
||||
];
|
||||
|
||||
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||
private tagPanelWatcherRef: string;
|
||||
private focusedElement = null;
|
||||
private isDoingStickyHeaders = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
searchFilter: "",
|
||||
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
||||
showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
|
||||
};
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||
this.setState({showTagPanel: 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() {
|
||||
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
private onSearch = (term: string): void => {
|
||||
this.setState({searchFilter: term});
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
};
|
||||
|
||||
private onBreadcrumbsUpdate = () => {
|
||||
const newVal = BreadcrumbsStore.instance.visible;
|
||||
if (newVal !== this.state.showBreadcrumbs) {
|
||||
this.setState({showBreadcrumbs: newVal});
|
||||
|
||||
// Update the sticky headers too as the breadcrumbs will be popping in or out.
|
||||
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
||||
this.handleStickyHeaders(this.listContainerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
private handleStickyHeaders(list: HTMLDivElement) {
|
||||
if (this.isDoingStickyHeaders) return;
|
||||
this.isDoingStickyHeaders = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
this.doStickyHeaders(list);
|
||||
this.isDoingStickyHeaders = false;
|
||||
});
|
||||
}
|
||||
|
||||
private doStickyHeaders(list: HTMLDivElement) {
|
||||
const topEdge = list.scrollTop;
|
||||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
||||
|
||||
const headerRightMargin = 16; // 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
|
||||
// excessive layout updates.
|
||||
const targetStyles = new Map<HTMLDivElement, {
|
||||
stickyTop?: boolean;
|
||||
stickyBottom?: boolean;
|
||||
makeInvisible?: boolean;
|
||||
}>();
|
||||
|
||||
let lastTopHeader;
|
||||
let firstBottomHeader;
|
||||
for (const sublist of sublists) {
|
||||
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
||||
header.style.removeProperty("display"); // always clear display:none first
|
||||
|
||||
// When an element is <=40% off screen, make it take over
|
||||
const offScreenFactor = 0.4;
|
||||
const isOffTop = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) <= topEdge;
|
||||
const isOffBottom = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) >= bottomEdge;
|
||||
|
||||
if (isOffTop || sublist === sublists[0]) {
|
||||
targetStyles.set(header, { stickyTop: true });
|
||||
if (lastTopHeader) {
|
||||
lastTopHeader.style.display = "none";
|
||||
targetStyles.set(lastTopHeader, { makeInvisible: true });
|
||||
}
|
||||
lastTopHeader = header;
|
||||
} else if (isOffBottom && !firstBottomHeader) {
|
||||
targetStyles.set(header, { stickyBottom: true });
|
||||
firstBottomHeader = header;
|
||||
} else {
|
||||
targetStyles.set(header, {}); // nothing == clear
|
||||
}
|
||||
}
|
||||
|
||||
// Run over the style changes and make them reality. We check to see if we're about to
|
||||
// cause a no-op update, as adding/removing properties that are/aren't there cause
|
||||
// layout updates.
|
||||
for (const header of targetStyles.keys()) {
|
||||
const style = targetStyles.get(header);
|
||||
|
||||
if (style.makeInvisible) {
|
||||
// we will have already removed the 'display: none', so add it back.
|
||||
header.style.display = "none";
|
||||
continue; // nothing else to do, even if sticky somehow
|
||||
}
|
||||
|
||||
if (style.stickyTop) {
|
||||
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||
}
|
||||
|
||||
const newTop = `${list.parentElement.offsetTop}px`;
|
||||
if (header.style.top !== newTop) {
|
||||
header.style.top = newTop;
|
||||
}
|
||||
} else {
|
||||
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||
}
|
||||
if (header.style.top) {
|
||||
header.style.removeProperty('top');
|
||||
}
|
||||
}
|
||||
|
||||
if (style.stickyBottom) {
|
||||
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||
}
|
||||
} else {
|
||||
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||
}
|
||||
}
|
||||
|
||||
if (style.stickyTop || style.stickyBottom) {
|
||||
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||
}
|
||||
|
||||
const newWidth = `${headerStickyWidth}px`;
|
||||
if (header.style.width !== newWidth) {
|
||||
header.style.width = newWidth;
|
||||
}
|
||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||
}
|
||||
if (header.style.width) {
|
||||
header.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add appropriate sticky classes to wrapper so it has
|
||||
// the necessary top/bottom padding to put the sticky header in
|
||||
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper
|
||||
if (lastTopHeader) {
|
||||
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||
} else {
|
||||
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||
}
|
||||
if (firstBottomHeader) {
|
||||
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||
} else {
|
||||
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||
}
|
||||
}
|
||||
|
||||
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||
const list = ev.target as HTMLDivElement;
|
||||
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) => {
|
||||
this.focusedElement = ev.target;
|
||||
};
|
||||
|
||||
private onBlur = () => {
|
||||
this.focusedElement = null;
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (!this.focusedElement) return;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN:
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.onMoveFocus(ev.key === Key.ARROW_UP);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private onEnter = () => {
|
||||
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2");
|
||||
if (firstRoom) {
|
||||
firstRoom.click();
|
||||
this.onSearch(""); // clear the search field
|
||||
}
|
||||
};
|
||||
|
||||
private onMoveFocus = (up: boolean) => {
|
||||
let element = this.focusedElement;
|
||||
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !cssClasses.some(c => classes.contains(c)));
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.focusedElement = element;
|
||||
}
|
||||
};
|
||||
|
||||
private renderHeader(): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_LeftPanel2_userHeader">
|
||||
<UserMenu isMinimized={this.props.isMinimized} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBreadcrumbs(): React.ReactNode {
|
||||
if (this.state.showBreadcrumbs && !this.props.isMinimized) {
|
||||
return (
|
||||
<IndicatorScrollbar
|
||||
className="mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar"
|
||||
verticalScrollsHorizontally={true}
|
||||
>
|
||||
<RoomBreadcrumbs2 />
|
||||
</IndicatorScrollbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSearchExplore(): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
className="mx_LeftPanel2_filterContainer"
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
<RoomSearch
|
||||
onQueryUpdate={this.onSearch}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onVerticalArrow={this.onKeyDown}
|
||||
onEnter={this.onEnter}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_LeftPanel2_exploreButton"
|
||||
onClick={this.onExplore}
|
||||
title={_t("Explore rooms")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const tagPanel = !this.state.showTagPanel ? null : (
|
||||
<div className="mx_LeftPanel2_tagPanelContainer">
|
||||
<TagPanel/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const roomList = <RoomList2
|
||||
onKeyDown={this.onKeyDown}
|
||||
resizeNotifier={null}
|
||||
collapsed={false}
|
||||
searchFilter={this.state.searchFilter}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.onResize}
|
||||
/>;
|
||||
|
||||
const containerClasses = classNames({
|
||||
"mx_LeftPanel2": true,
|
||||
"mx_LeftPanel2_hasTagPanel": !!tagPanel,
|
||||
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
||||
});
|
||||
|
||||
const roomListClasses = classNames(
|
||||
"mx_LeftPanel2_actualRoomListContainer",
|
||||
"mx_AutoHideScrollbar",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{tagPanel}
|
||||
<aside className="mx_LeftPanel2_roomListContainer">
|
||||
{this.renderHeader()}
|
||||
{this.renderSearchExplore()}
|
||||
{this.renderBreadcrumbs()}
|
||||
<div className="mx_LeftPanel2_roomListWrapper">
|
||||
<div
|
||||
className={roomListClasses}
|
||||
onScroll={this.onScroll}
|
||||
ref={this.listContainerRef}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
>
|
||||
{roomList}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2017, 2018, 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,29 +16,45 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
import { getHomePageUrl } from '../../utils/pages';
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
|
||||
import HomePage from "./HomePage";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
|
||||
import { DefaultTagID } from "../../stores/room-list/models";
|
||||
import {
|
||||
showToast as showSetPasswordToast,
|
||||
hideToast as hideSetPasswordToast
|
||||
} from "../../toasts/SetPasswordToast";
|
||||
import {
|
||||
showToast as showServerLimitToast,
|
||||
hideToast as hideServerLimitToast
|
||||
} from "../../toasts/ServerLimitToast";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import LeftPanel2 from "./LeftPanel2";
|
||||
import CallContainer from '../views/voip/CallContainer';
|
||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
|
@ -51,6 +67,54 @@ function canElementReceiveInput(el) {
|
|||
!!el.getAttribute("contenteditable");
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
viaServers?: string[];
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
middleDisabled: boolean;
|
||||
initialEventPixelOffset: number;
|
||||
leftDisabled: boolean;
|
||||
rightDisabled: boolean;
|
||||
page_type: string;
|
||||
autoJoin: boolean;
|
||||
thirdPartyInvite?: object;
|
||||
roomOobData?: object;
|
||||
currentRoomId: string;
|
||||
ConferenceHandler?: object;
|
||||
collapseLhs: boolean;
|
||||
config: {
|
||||
piwik: {
|
||||
policyUrl: string;
|
||||
},
|
||||
[key: string]: any,
|
||||
};
|
||||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
}
|
||||
|
||||
interface IUsageLimit {
|
||||
limit_type: "monthly_active_user" | string;
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
mouseDown?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
syncErrorData?: {
|
||||
error: {
|
||||
data: IUsageLimit;
|
||||
errcode: string;
|
||||
};
|
||||
};
|
||||
usageLimitEventContent?: IUsageLimit;
|
||||
useCompactLayout: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -60,10 +124,10 @@ function canElementReceiveInput(el) {
|
|||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
const LoggedInView = createReactClass({
|
||||
displayName: 'LoggedInView',
|
||||
class LoggedInView extends React.Component<IProps, IState> {
|
||||
static displayName = 'LoggedInView';
|
||||
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
|
@ -76,24 +140,26 @@ const LoggedInView = createReactClass({
|
|||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
protected readonly _matrixClient: MatrixClient;
|
||||
protected readonly _roomView: React.RefObject<any>;
|
||||
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
|
||||
protected readonly _sessionStore: sessionStore;
|
||||
protected readonly _sessionStoreToken: { remove: () => void };
|
||||
protected readonly _compactLayoutWatcherRef: string;
|
||||
protected resizer: Resizer;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
mouseDown: undefined,
|
||||
syncErrorData: undefined,
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
|
@ -113,33 +179,33 @@ const LoggedInView = createReactClass({
|
|||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
|
||||
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
|
||||
"useCompactLayout", null, this.onCompactLayoutChanged,
|
||||
);
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = createRef();
|
||||
},
|
||||
this._roomView = React.createRef();
|
||||
this._resizeContainer = React.createRef();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// attempt to guess when a banner was opened or closed
|
||||
if (
|
||||
(prevProps.showCookieBar !== this.props.showCookieBar) ||
|
||||
(prevProps.hasNewVersion !== this.props.hasNewVersion) ||
|
||||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) ||
|
||||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
|
||||
) {
|
||||
this.props.resizeNotifier.notifyBannersChanged();
|
||||
}
|
||||
},
|
||||
componentDidMount() {
|
||||
this.resizer = this._createResizer();
|
||||
this.resizer.attach();
|
||||
this._loadResizerPreferences();
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
this.resizer.detach();
|
||||
},
|
||||
}
|
||||
|
||||
// Child components assume that the client peg will not be null, so give them some
|
||||
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||
|
@ -147,22 +213,24 @@ const LoggedInView = createReactClass({
|
|||
// This is required because `LoggedInView` maintains its own state and if this state
|
||||
// updates after the client peg has been made null (during logout), then it will
|
||||
// attempt to re-render and the children will throw errors.
|
||||
shouldComponentUpdate: function() {
|
||||
shouldComponentUpdate() {
|
||||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
}
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
canResetTimelineInRoom = (roomId) => {
|
||||
if (!this._roomView.current) {
|
||||
return true;
|
||||
}
|
||||
return this._roomView.current.canResetTimeline();
|
||||
},
|
||||
};
|
||||
|
||||
_setStateFromSessionStore() {
|
||||
this.setState({
|
||||
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
|
||||
});
|
||||
},
|
||||
_setStateFromSessionStore = () => {
|
||||
if (this._sessionStore.getCachedPassword()) {
|
||||
showSetPasswordToast();
|
||||
} else {
|
||||
hideSetPasswordToast();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
const classNames = {
|
||||
|
@ -186,35 +254,34 @@ const LoggedInView = createReactClass({
|
|||
},
|
||||
};
|
||||
const resizer = new Resizer(
|
||||
this.resizeContainer,
|
||||
this._resizeContainer.current,
|
||||
CollapseDistributor,
|
||||
collapseConfig);
|
||||
resizer.setClassNames(classNames);
|
||||
return resizer;
|
||||
},
|
||||
}
|
||||
|
||||
_loadResizerPreferences() {
|
||||
let lhsSize = window.localStorage.getItem("mx_lhs_size");
|
||||
if (lhsSize !== null) {
|
||||
lhsSize = parseInt(lhsSize, 10);
|
||||
} else {
|
||||
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
|
||||
if (isNaN(lhsSize)) {
|
||||
lhsSize = 350;
|
||||
}
|
||||
this.resizer.forHandleAt(0).resize(lhsSize);
|
||||
},
|
||||
}
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "im.vector.web.settings") {
|
||||
this.setState({
|
||||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
onAccountData = (event) => {
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({action: "ignore_state_changed"});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onSync: function(syncState, oldSyncState, data) {
|
||||
onCompactLayoutChanged = (setting, roomId, level, valueAtLevel, newValue) => {
|
||||
this.setState({
|
||||
useCompactLayout: valueAtLevel,
|
||||
});
|
||||
};
|
||||
|
||||
onSync = (syncState, oldSyncState, data) => {
|
||||
const oldErrCode = (
|
||||
this.state.syncErrorData &&
|
||||
this.state.syncErrorData.error &&
|
||||
|
@ -235,22 +302,37 @@ const LoggedInView = createReactClass({
|
|||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
} else {
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_updateServerNoticeEvents: async function() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
_calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
|
||||
const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
|
||||
if (error) {
|
||||
usageLimitEventContent = syncErrorData.error.data;
|
||||
}
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
if (usageLimitEventContent) {
|
||||
showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error);
|
||||
} else {
|
||||
hideServerLimitToast();
|
||||
}
|
||||
}
|
||||
|
||||
_updateServerNoticeEvents = async () => {
|
||||
const roomLists = RoomListStoreTempProxy.getRoomLists();
|
||||
if (!roomLists[DefaultTagID.ServerNotice]) return [];
|
||||
|
||||
const events = [];
|
||||
for (const room of roomLists[DefaultTagID.ServerNotice]) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
@ -258,16 +340,23 @@ const LoggedInView = createReactClass({
|
|||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (ev) pinnedEvents.push(ev);
|
||||
const event = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (event) events.push(event);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
});
|
||||
},
|
||||
|
||||
_onPaste: function(ev) {
|
||||
const usageLimitEvent = events.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
|
||||
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
|
||||
this.setState({ usageLimitEventContent });
|
||||
};
|
||||
|
||||
_onPaste = (ev) => {
|
||||
let canReceiveInput = false;
|
||||
let element = ev.target;
|
||||
// test for all parents because the target can be a child of a contenteditable element
|
||||
|
@ -279,9 +368,9 @@ const LoggedInView = createReactClass({
|
|||
// refocusing during a paste event will make the
|
||||
// paste end up in the newly focused element,
|
||||
// so dispatch synchronously before paste happens
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
dis.fire(Action.FocusComposer, true);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
SOME HACKERY BELOW:
|
||||
|
@ -305,45 +394,31 @@ const LoggedInView = createReactClass({
|
|||
We also listen with a native listener on the document to get keydown events when no element is focused.
|
||||
Bubbling is irrelevant here as the target is the body element.
|
||||
*/
|
||||
_onReactKeyDown: function(ev) {
|
||||
_onReactKeyDown = (ev) => {
|
||||
// events caught while bubbling up on the root element
|
||||
// of this component, so something must be focused.
|
||||
this._onKeyDown(ev);
|
||||
},
|
||||
};
|
||||
|
||||
_onNativeKeyDown: function(ev) {
|
||||
_onNativeKeyDown = (ev) => {
|
||||
// only pass this if there is no focused element.
|
||||
// if there is, _onKeyDown will be called by the
|
||||
// react keydown handler that respects the react bubbling order.
|
||||
if (ev.target === document.body) {
|
||||
this._onKeyDown(ev);
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
// Will need to find a better meta key if anyone actually cares about using this.
|
||||
if (ev.altKey && ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) {
|
||||
dis.dispatch({
|
||||
action: 'view_indexed_room',
|
||||
roomIndex: ev.keyCode - 49,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
_onKeyDown = (ev) => {
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey ||
|
||||
ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!hasModifier) {
|
||||
if (!hasModifier && !isModifier) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
|
@ -365,26 +440,58 @@ const LoggedInView = createReactClass({
|
|||
}
|
||||
break;
|
||||
case Key.BACKTICK:
|
||||
if (ev.key !== "`") break;
|
||||
|
||||
// Ideally this would be CTRL+P for "Profile", but that's
|
||||
// taken by the print dialog. CTRL+I for "Information"
|
||||
// was previously chosen but conflicted with italics in
|
||||
// composer, so CTRL+` it is
|
||||
|
||||
if (ctrlCmdOnly) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_top_left_menu',
|
||||
dis.fire(Action.ToggleUserMenu);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.SLASH:
|
||||
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
|
||||
KeyboardShortcuts.toggleDialog();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
case Key.ARROW_DOWN:
|
||||
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
dis.dispatch<ViewRoomDeltaPayload>({
|
||||
action: Action.ViewRoomDelta,
|
||||
delta: ev.key === Key.ARROW_UP ? -1 : 1,
|
||||
unread: ev.shiftKey,
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case Key.PERIOD:
|
||||
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
|
||||
dis.dispatch({
|
||||
action: 'toggle_right_panel',
|
||||
type: this.props.page_type === "room_view" ? "room" : "group",
|
||||
});
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// if we do not have a handler for it, pass it to the platform which might
|
||||
handled = PlatformPeg.get().onKeyDown(ev);
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (!hasModifier) {
|
||||
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
// The above condition is crafted to _allow_ characters with Shift
|
||||
// already pressed (but not the Shift key down itself).
|
||||
|
||||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
|
@ -395,25 +502,25 @@ const LoggedInView = createReactClass({
|
|||
|
||||
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
|
||||
// synchronous dispatch so we focus before key generates input
|
||||
dis.dispatch({action: 'focus_composer'}, true);
|
||||
dis.fire(Action.FocusComposer, true);
|
||||
ev.stopPropagation();
|
||||
// we should *not* preventDefault() here as
|
||||
// that would prevent typing in the now-focussed composer
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* dispatch a page-up/page-down/etc to the appropriate component
|
||||
* @param {Object} ev The key event
|
||||
*/
|
||||
_onScrollKeyPressed: function(ev) {
|
||||
_onScrollKeyPressed = (ev) => {
|
||||
if (this._roomView.current) {
|
||||
this._roomView.current.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onDragEnd: function(result) {
|
||||
_onDragEnd = (result) => {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
|
@ -436,9 +543,9 @@ const LoggedInView = createReactClass({
|
|||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onRoomTileEndDrag: function(result) {
|
||||
_onRoomTileEndDrag = (result) => {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
|
@ -455,9 +562,9 @@ const LoggedInView = createReactClass({
|
|||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
},
|
||||
};
|
||||
|
||||
_onMouseDown: function(ev) {
|
||||
_onMouseDown = (ev) => {
|
||||
// When the panels are disabled, clicking on them results in a mouse event
|
||||
// which bubbles to certain elements in the tree. When this happens, close
|
||||
// any settings page that is currently open (user/room/group).
|
||||
|
@ -476,9 +583,9 @@ const LoggedInView = createReactClass({
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_onMouseUp: function(ev) {
|
||||
_onMouseUp = (ev) => {
|
||||
if (!this.state.mouseDown) return;
|
||||
|
||||
const deltaX = ev.pageX - this.state.mouseDown.x;
|
||||
|
@ -497,26 +604,15 @@ const LoggedInView = createReactClass({
|
|||
// Always clear the mouseDown state to ensure we don't accidentally
|
||||
// use stale values due to the mouseDown checks.
|
||||
this.setState({mouseDown: null});
|
||||
},
|
||||
};
|
||||
|
||||
_setResizeContainerRef(div) {
|
||||
this.resizeContainer = div;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserView = sdk.getComponent('structures.UserView');
|
||||
const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage');
|
||||
const GroupView = sdk.getComponent('structures.GroupView');
|
||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||
const ToastContainer = sdk.getComponent('structures.ToastContainer');
|
||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let pageElement;
|
||||
|
||||
|
@ -546,13 +642,7 @@ const LoggedInView = createReactClass({
|
|||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
{
|
||||
const pageUrl = getHomePageUrl(this.props.config);
|
||||
pageElement = <EmbeddedPage className="mx_HomePage"
|
||||
url={pageUrl}
|
||||
scrollbar={true}
|
||||
/>;
|
||||
}
|
||||
pageElement = <HomePage />;
|
||||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
|
@ -566,49 +656,27 @@ const LoggedInView = createReactClass({
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
} else if (this.props.hasNewVersion) {
|
||||
topBar = <NewVersionBar version={this.props.version} newVersion={this.props.newVersion}
|
||||
releaseNotes={this.props.newVersionReleaseNotes}
|
||||
/>;
|
||||
} else if (this.props.checkingForUpdate) {
|
||||
topBar = <UpdateCheckBar {...this.props.checkingForUpdate} />;
|
||||
} else if (this.state.userHasGeneratedPassword) {
|
||||
topBar = <PasswordNagBar />;
|
||||
} else if (this.props.showNotifierToolbar) {
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
if (this.state.useCompactLayout) {
|
||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||
}
|
||||
|
||||
let leftPanel = (
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
);
|
||||
if (SettingsStore.getValue("feature_new_room_list")) {
|
||||
leftPanel = (
|
||||
<LeftPanel2
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||
<div
|
||||
|
@ -619,23 +687,19 @@ const LoggedInView = createReactClass({
|
|||
onMouseDown={this._onMouseDown}
|
||||
onMouseUp={this._onMouseUp}
|
||||
>
|
||||
{ topBar }
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ leftPanel }
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
<CallContainer />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default LoggedInView;
|
|
@ -93,14 +93,19 @@ export default class MainSplit extends React.Component {
|
|||
const bodyView = React.Children.only(this.props.children);
|
||||
const panelView = this.props.panel;
|
||||
|
||||
if (this.props.collapsedRhs || !panelView) {
|
||||
return bodyView;
|
||||
} else {
|
||||
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
|
||||
{ bodyView }
|
||||
const hasResizer = !this.props.collapsedRhs && panelView;
|
||||
|
||||
let children;
|
||||
if (hasResizer) {
|
||||
children = <React.Fragment>
|
||||
<ResizeHandle reverse={true} />
|
||||
{ panelView }
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
|
||||
{ bodyView }
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,10 +28,36 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
|
|||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
// check if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
function shouldFormContinuation(prevEvent, mxEvent) {
|
||||
// sanity check inputs
|
||||
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
|
||||
// check if within the max continuation period
|
||||
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
|
||||
|
||||
// Some events should appear as continuations from previous events of different types.
|
||||
if (mxEvent.getType() !== prevEvent.getType() &&
|
||||
(!continuedTypes.includes(mxEvent.getType()) ||
|
||||
!continuedTypes.includes(prevEvent.getType()))) return false;
|
||||
|
||||
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
|
||||
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
|
||||
mxEvent.sender.name !== prevEvent.sender.name ||
|
||||
mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
|
||||
|
||||
// if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
|
||||
if (!haveTileForEvent(prevEvent)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
|
@ -106,10 +132,14 @@ export default class MessagePanel extends React.Component {
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Force props to be loaded for useIRCLayout
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// previous positions the read marker has been in, so we can
|
||||
|
@ -358,8 +388,11 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<li key={"readMarker_"+eventId} ref={this._readMarkerNode}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
<li key={"readMarker_"+eventId}
|
||||
ref={this._readMarkerNode}
|
||||
className="mx_RoomView_myReadMarker_container"
|
||||
data-scroll-tokens={eventId}
|
||||
>
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
|
@ -502,44 +535,13 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
_getTilesForEvent(prevEvent, mxEv, last) {
|
||||
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const ret = [];
|
||||
|
||||
const isEditing = this.props.editState &&
|
||||
this.props.editState.getEvent().getId() === mxEv.getId();
|
||||
// is this a continuation of the previous message?
|
||||
let continuation = false;
|
||||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
/*
|
||||
// Work out if this is still a continuation, as we are now showing commands
|
||||
// and /me messages with their own little avatar. The case of a change of
|
||||
// event type (commands) is handled above, but we need to handle the /me
|
||||
// messages seperately as they have a msgtype of 'm.emote' but are classed
|
||||
// as normal messages
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& mxEv.getType() == prevEvent.getType()
|
||||
&& prevEvent.getContent().msgtype === 'm.emote') {
|
||||
continuation = false;
|
||||
}
|
||||
*/
|
||||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
|
@ -551,12 +553,15 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||
if (wantsDateSeparator) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
|
@ -575,25 +580,28 @@ export default class MessagePanel extends React.Component {
|
|||
ref={this._collectEventNode.bind(this, eventId)}
|
||||
data-scroll-tokens={scrollToken}
|
||||
>
|
||||
<EventTile mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting.bind(this)}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
/>
|
||||
<TileErrorBoundary mxEvent={mxEv}>
|
||||
<EventTile mxEvent={mxEv}
|
||||
continuation={continuation}
|
||||
isRedacted={mxEv.isRedacted()}
|
||||
replacingEventId={mxEv.replacingEventId()}
|
||||
editState={isEditing && this.props.editState}
|
||||
onHeightChanged={this._onHeightChanged}
|
||||
readReceipts={readReceipts}
|
||||
readReceiptMap={this._readReceiptMap}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
checkUnmounting={this._isUnmounting.bind(this)}
|
||||
eventSendStatus={mxEv.getAssociatedStatus()}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
last={last}
|
||||
isSelectedEvent={highlight}
|
||||
getRelationsForEvent={this.props.getRelationsForEvent}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
</TileErrorBoundary>
|
||||
</li>,
|
||||
);
|
||||
|
||||
|
@ -755,6 +763,7 @@ export default class MessagePanel extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
@ -786,23 +795,35 @@ export default class MessagePanel extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let ircResizer = null;
|
||||
if (this.props.useIRCLayout) {
|
||||
ircResizer = <IRCTimelineProfileResizer
|
||||
minWidth={20}
|
||||
maxWidth={600}
|
||||
roomId={this.props.room ? this.props.room.roomId : null}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
<ErrorBoundary>
|
||||
<ScrollPanel
|
||||
ref={this._scrollPanel}
|
||||
className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
fixedChildren={ircResizer}
|
||||
>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ whoIsTyping }
|
||||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -836,14 +857,16 @@ class CreationGrouper {
|
|||
// events that we include in the group but then eject out and place
|
||||
// above the group.
|
||||
this.ejectedEvents = [];
|
||||
this.readMarker = panel._readMarkerForEvent(createEvent.getId());
|
||||
this.readMarker = panel._readMarkerForEvent(
|
||||
createEvent.getId(),
|
||||
createEvent === lastShownEvent,
|
||||
);
|
||||
}
|
||||
|
||||
shouldGroup(ev) {
|
||||
const panel = this.panel;
|
||||
const createEvent = this.createEvent;
|
||||
if (!panel._shouldShowEvent(ev)) {
|
||||
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
|
||||
return true;
|
||||
}
|
||||
if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
|
||||
|
@ -861,7 +884,10 @@ class CreationGrouper {
|
|||
|
||||
add(ev) {
|
||||
const panel = this.panel;
|
||||
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
|
||||
this.readMarker = this.readMarker || panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
ev === this.lastShownEvent,
|
||||
);
|
||||
if (!panel._shouldShowEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
@ -948,18 +974,34 @@ class MemberGrouper {
|
|||
|
||||
constructor(panel, ev, prevEvent, lastShownEvent) {
|
||||
this.panel = panel;
|
||||
this.readMarker = panel._readMarkerForEvent(ev.getId());
|
||||
this.readMarker = panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
ev === lastShownEvent,
|
||||
);
|
||||
this.events = [ev];
|
||||
this.prevEvent = prevEvent;
|
||||
this.lastShownEvent = lastShownEvent;
|
||||
}
|
||||
|
||||
shouldGroup(ev) {
|
||||
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return isMembershipChange(ev);
|
||||
}
|
||||
|
||||
add(ev) {
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
|
||||
if (ev.getType() === 'm.room.member') {
|
||||
// We'll just double check that it's worth our time to do so, through an
|
||||
// ugly hack. If textForEvent returns something, we should group it for
|
||||
// rendering but if it doesn't then we'll exclude it.
|
||||
const renderText = textForEvent(ev);
|
||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
||||
}
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
|
||||
ev.getId(),
|
||||
ev === this.lastShownEvent,
|
||||
);
|
||||
this.events.push(ev);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,9 +20,11 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MyGroups',
|
||||
|
@ -37,7 +40,7 @@ export default createReactClass({
|
|||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._fetch();
|
||||
},
|
||||
|
||||
|
@ -59,11 +62,10 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
|
||||
let content;
|
||||
let contentHeader;
|
||||
|
@ -74,11 +76,12 @@ export default createReactClass({
|
|||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbarWrapper>
|
||||
<AutoHideScrollbar className="mx_MyGroups_scrollable">
|
||||
<div className="mx_MyGroups_microcopy">
|
||||
<p>
|
||||
{ _t(
|
||||
"Did you know: you can use communities to filter your Riot.im experience!",
|
||||
"Did you know: you can use communities to filter your %(brand)s experience!",
|
||||
{ brand },
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
|
@ -93,7 +96,7 @@ export default createReactClass({
|
|||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbarWrapper> :
|
||||
</AutoHideScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
|
|
|
@ -22,14 +22,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import RateLimitedFunc from '../../ratelimitedfunc';
|
||||
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
export default class RightPanel extends React.Component {
|
||||
static get propTypes() {
|
||||
|
@ -108,7 +108,7 @@ export default class RightPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const cli = this.context;
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -123,7 +123,8 @@ export default class RightPanel extends React.Component {
|
|||
this._unregisterGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
|
@ -182,20 +183,42 @@ export default class RightPanel extends React.Component {
|
|||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onCloseUserInfo = () => {
|
||||
// XXX: There are three different ways of 'closing' this panel depending on what state
|
||||
// things are in... this knows far more than it should do about the state of the rest
|
||||
// of the app and is generally a bit silly.
|
||||
if (this.props.user) {
|
||||
// If we have a user prop then we're displaying a user from the 'user' page type
|
||||
// in LoggedInView, so need to change the page type to close the panel (we switch
|
||||
// to the home page which is not obviously the correct thing to do, but I'm not sure
|
||||
// anything else is - we could hide the close button altogether?)
|
||||
dis.dispatch({
|
||||
action: "view_home_page",
|
||||
});
|
||||
} else {
|
||||
// Otherwise we have got our user from RoomViewStore which means we're being shown
|
||||
// within a room/group, so go back to the member panel if we were in the encryption panel,
|
||||
// or the member list if we were in the member panel... phew.
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const MemberList = sdk.getComponent('rooms.MemberList');
|
||||
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
const UserInfo = sdk.getComponent('right_panel.UserInfo');
|
||||
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
|
||||
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
|
||||
const FilePanel = sdk.getComponent('structures.FilePanel');
|
||||
|
||||
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
|
||||
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
|
||||
const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
|
||||
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
|
||||
|
||||
|
@ -217,53 +240,25 @@ export default class RightPanel extends React.Component {
|
|||
break;
|
||||
case RIGHT_PANEL_PHASES.RoomMemberInfo:
|
||||
case RIGHT_PANEL_PHASES.EncryptionPanel:
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo
|
||||
member={this.state.member}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
/>;
|
||||
}
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
roomId={this.props.roomId}
|
||||
key={this.props.roomId || this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.Room3pidMemberInfo:
|
||||
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.roomId} />;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.GroupMemberInfo:
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
const onClose = () => {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
member: null,
|
||||
});
|
||||
};
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={onClose} />;
|
||||
} else {
|
||||
panel = (
|
||||
<GroupMemberInfo
|
||||
groupMember={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
panel = <UserInfo
|
||||
user={this.state.member}
|
||||
groupId={this.props.groupId}
|
||||
key={this.state.member.userId}
|
||||
onClose={this.onCloseUserInfo} />;
|
||||
break;
|
||||
case RIGHT_PANEL_PHASES.GroupRoomInfo:
|
||||
panel = <GroupRoomInfo
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,14 +20,16 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import * as sdk from "../../index";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
|
||||
import Analytics from '../../Analytics';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 160;
|
||||
|
@ -40,30 +42,23 @@ export default createReactClass({
|
|||
displayName: 'RoomDirectory',
|
||||
|
||||
propTypes: {
|
||||
config: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
config: {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
publicRooms: [],
|
||||
loading: true,
|
||||
protocolsLoading: true,
|
||||
error: null,
|
||||
instanceId: null,
|
||||
includeAll: false,
|
||||
roomServer: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this.nextBatch = null;
|
||||
this.filterTimeout = null;
|
||||
|
@ -80,7 +75,7 @@ export default createReactClass({
|
|||
this.protocols = response;
|
||||
this.setState({protocolsLoading: false});
|
||||
}, (err) => {
|
||||
console.warn(`error loading thirdparty protocols: ${err}`);
|
||||
console.warn(`error loading third party protocols: ${err}`);
|
||||
this.setState({protocolsLoading: false});
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// Guests currently aren't allowed to use this API, so
|
||||
|
@ -89,13 +84,17 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
track('Failed to get protocol list from homeserver');
|
||||
const brand = SdkConfig.get().brand;
|
||||
this.setState({
|
||||
error: _t(
|
||||
'Riot failed to get the protocol list from the homeserver. ' +
|
||||
'%(brand)s failed to get the protocol list from the homeserver. ' +
|
||||
'The homeserver may be too old to support third party networks.',
|
||||
{ brand },
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
this.refreshRoomList();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -130,10 +129,10 @@ export default createReactClass({
|
|||
if (my_server != MatrixClientPeg.getHomeserverName()) {
|
||||
opts.server = my_server;
|
||||
}
|
||||
if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
} else if (this.state.includeAll) {
|
||||
if (this.state.instanceId === ALL_ROOMS) {
|
||||
opts.include_all_networks = true;
|
||||
} else if (this.state.instanceId) {
|
||||
opts.third_party_instance_id = this.state.instanceId;
|
||||
}
|
||||
if (this.nextBatch) opts.since = this.nextBatch;
|
||||
if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
|
||||
|
@ -177,12 +176,13 @@ export default createReactClass({
|
|||
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
track('Failed to get public room list');
|
||||
const brand = SdkConfig.get().brand;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error:
|
||||
`${_t('Riot failed to get the public room list.')} ` +
|
||||
`${(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')}`
|
||||
,
|
||||
error: (
|
||||
_t('%(brand)s failed to get the public room list.', { brand }) +
|
||||
(err && err.message) ? err.message : _t('The homeserver may be unavailable or overloaded.')
|
||||
),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -203,7 +203,7 @@ export default createReactClass({
|
|||
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||
} else {
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ export default createReactClass({
|
|||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
step = _t('delete the address.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).then(() => {
|
||||
modal.close();
|
||||
|
@ -247,7 +247,7 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
onOptionChange: function(server, instanceId, includeAll) {
|
||||
onOptionChange: function(server, instanceId) {
|
||||
// clear next batch so we don't try to load more rooms
|
||||
this.nextBatch = null;
|
||||
this.setState({
|
||||
|
@ -257,7 +257,6 @@ export default createReactClass({
|
|||
publicRooms: [],
|
||||
roomServer: server,
|
||||
instanceId: instanceId,
|
||||
includeAll: includeAll,
|
||||
error: null,
|
||||
}, this.refreshRoomList);
|
||||
// We also refresh the room list each time even though this
|
||||
|
@ -305,7 +304,7 @@ export default createReactClass({
|
|||
|
||||
onJoinFromSearchClick: function(alias) {
|
||||
// If we don't have a particular instance id selected, just show that rooms alias
|
||||
if (!this.state.instanceId) {
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
// If the user specified an alias without a domain, add on whichever server is selected
|
||||
// in the dropdown
|
||||
if (alias.indexOf(':') == -1) {
|
||||
|
@ -319,9 +318,10 @@ export default createReactClass({
|
|||
const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
|
||||
if (!fields) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const brand = SdkConfig.get().brand;
|
||||
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
|
||||
title: _t('Unable to join network'),
|
||||
description: _t('Riot does not know how to join a room on this network'),
|
||||
description: _t('%(brand)s does not know how to join a room on this network', { brand }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -372,7 +372,10 @@ export default createReactClass({
|
|||
|
||||
onCreateRoomClick: function(room) {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({action: 'view_create_room'});
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
public: true,
|
||||
});
|
||||
},
|
||||
|
||||
showRoomAlias: function(alias, autoJoin=false) {
|
||||
|
@ -406,6 +409,12 @@ export default createReactClass({
|
|||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
if (this.state.roomServer) {
|
||||
payload.opts = {
|
||||
viaServers: [this.state.roomServer],
|
||||
};
|
||||
}
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
|
@ -587,7 +596,7 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
let placeholder = _t('Find a room…');
|
||||
if (!this.state.instanceId) {
|
||||
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
|
||||
placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
|
||||
} else if (instance_expected_field_type) {
|
||||
placeholder = instance_expected_field_type.placeholder;
|
||||
|
@ -604,10 +613,18 @@ export default createReactClass({
|
|||
listHeader = <div className="mx_RoomDirectory_listheader">
|
||||
<DirectorySearchBox
|
||||
className="mx_RoomDirectory_searchbox"
|
||||
onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder} showJoinButton={showJoinButton}
|
||||
onChange={this.onFilterChange}
|
||||
onClear={this.onFilterClear}
|
||||
onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder}
|
||||
showJoinButton={showJoinButton}
|
||||
/>
|
||||
<NetworkDropdown
|
||||
protocols={this.protocols}
|
||||
onOptionChange={this.onOptionChange}
|
||||
selectedServerName={this.state.roomServer}
|
||||
selectedInstanceId={this.state.instanceId}
|
||||
/>
|
||||
<NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
|
||||
</div>;
|
||||
}
|
||||
const explanation =
|
||||
|
@ -628,7 +645,7 @@ export default createReactClass({
|
|||
title={_t("Explore rooms")}
|
||||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
<p>{explanation}</p>
|
||||
{explanation}
|
||||
<div className="mx_RoomDirectory_list">
|
||||
{listHeader}
|
||||
{content}
|
||||
|
|
172
src/components/structures/RoomSearch.tsx
Normal file
172
src/components/structures/RoomSearch.tsx
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { throttle } from 'lodash';
|
||||
import { Key } from "../../Keyboard";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
||||
interface IProps {
|
||||
onQueryUpdate: (newQuery: string) => void;
|
||||
isMinimized: boolean;
|
||||
onVerticalArrow(ev: React.KeyboardEvent);
|
||||
onEnter(ev: React.KeyboardEvent);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
query: string;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
query: "",
|
||||
focused: false,
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'view_room' && payload.clear_search) {
|
||||
this.clearInput();
|
||||
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private clearInput = () => {
|
||||
if (!this.inputRef.current) return;
|
||||
this.inputRef.current.value = "";
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
private openSearch = () => {
|
||||
defaultDispatcher.dispatch({action: "show_left_panel"});
|
||||
defaultDispatcher.dispatch({action: "focus_room_filter"});
|
||||
};
|
||||
|
||||
private onChange = () => {
|
||||
if (!this.inputRef.current) return;
|
||||
this.setState({query: this.inputRef.current.value});
|
||||
this.onSearchUpdated();
|
||||
};
|
||||
|
||||
// it wants this at the top of the file, but we know better
|
||||
// tslint:disable-next-line
|
||||
private onSearchUpdated = throttle(
|
||||
() => {
|
||||
// We can't use the state variable because it can lag behind the input.
|
||||
// The lag is most obvious when deleting/clearing text with the keyboard.
|
||||
this.props.onQueryUpdate(this.inputRef.current.value);
|
||||
}, 200, {trailing: true, leading: true},
|
||||
);
|
||||
|
||||
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
this.setState({focused: true});
|
||||
ev.target.select();
|
||||
};
|
||||
|
||||
private onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
this.setState({focused: false});
|
||||
};
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
this.clearInput();
|
||||
defaultDispatcher.fire(Action.FocusComposer);
|
||||
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) {
|
||||
this.props.onVerticalArrow(ev);
|
||||
} else if (ev.key === Key.ENTER) {
|
||||
this.props.onEnter(ev);
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const classes = classNames({
|
||||
'mx_RoomSearch': true,
|
||||
'mx_RoomSearch_expanded': this.state.query || this.state.focused,
|
||||
'mx_RoomSearch_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
const inputClasses = classNames({
|
||||
'mx_RoomSearch_input': true,
|
||||
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
||||
});
|
||||
|
||||
let icon = (
|
||||
<div className='mx_RoomSearch_icon'/>
|
||||
);
|
||||
let input = (
|
||||
<input
|
||||
type="text"
|
||||
ref={this.inputRef}
|
||||
className={inputClasses}
|
||||
value={this.state.query}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={_t("Search")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
let clearButton = (
|
||||
<AccessibleButton
|
||||
tabIndex={-1}
|
||||
title={_t("Clear filter")}
|
||||
className="mx_RoomSearch_clearButton"
|
||||
onClick={this.clearInput}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.isMinimized) {
|
||||
icon = (
|
||||
<AccessibleButton
|
||||
title={_t("Search rooms")}
|
||||
className="mx_RoomSearch_icon"
|
||||
onClick={this.openSearch}
|
||||
/>
|
||||
);
|
||||
input = null;
|
||||
clearButton = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{icon}
|
||||
{input}
|
||||
{clearButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -24,9 +24,9 @@ import { _t, _td } from '../../languageHandler';
|
|||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -96,7 +96,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
|
||||
|
@ -126,25 +126,14 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
});
|
||||
},
|
||||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||
|
@ -184,7 +173,7 @@ export default createReactClass({
|
|||
if (this.props.hasActiveCall) {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return (
|
||||
<TintableSvg src={require("../../../res/img/sound-indicator.svg")} width="23" height="20" />
|
||||
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -213,82 +202,65 @@ export default createReactClass({
|
|||
if (!unsentMessages.length) return null;
|
||||
|
||||
let title;
|
||||
let content;
|
||||
|
||||
const hasUDE = unsentMessages.some((m) => {
|
||||
return m.error && m.error.name === "UnknownDeviceError";
|
||||
});
|
||||
|
||||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown sessions being present");
|
||||
content = _t(
|
||||
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
|
||||
let consentError = null;
|
||||
let resourceLimitError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
|
||||
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
unsentMessages[0].error.data.error
|
||||
) {
|
||||
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
|
||||
} else {
|
||||
let consentError = null;
|
||||
let resourceLimitError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
unsentMessages[0].error.data.error
|
||||
) {
|
||||
title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
|
||||
} else {
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||
"You can also select individual messages to resend or cancel.",
|
||||
{ count: unsentMessages.length },
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
'cancelText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
|
||||
const content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> " +
|
||||
"now. You can also select individual messages to resend or cancel.",
|
||||
{ count: unsentMessages.length },
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
'cancelText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||
},
|
||||
);
|
||||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src={require("../../../res/img/feather-customised/warning-triangle.svg")} width="24" height="24" title={_t("Warning")} alt="" />
|
||||
<div>
|
||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import * as Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
@ -32,10 +32,47 @@ import RoomTile from "../views/rooms/RoomTile";
|
|||
import LazyRenderList from "../views/elements/LazyRenderList";
|
||||
import {_t} from "../../languageHandler";
|
||||
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
|
||||
import {toPx} from "../../utils/units";
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
const debug = false;
|
||||
|
||||
class RoomTileErrorBoundary extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Side effects are not permitted here, so we only update the state so
|
||||
// that the next render shows an error message.
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, { componentStack }) {
|
||||
// Browser consoles are better at formatting output when native errors are passed
|
||||
// in their own `console.error` invocation.
|
||||
console.error(error);
|
||||
console.error(
|
||||
"The above error occured while React was rendering the following components:",
|
||||
componentStack,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (<div className="mx_RoomTile mx_RoomTileError">
|
||||
{this.props.roomId}
|
||||
</div>);
|
||||
} else {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class RoomSubList extends React.PureComponent {
|
||||
static displayName = 'RoomSubList';
|
||||
static debug = debug;
|
||||
|
@ -46,8 +83,6 @@ export default class RoomSubList extends React.PureComponent {
|
|||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
|
@ -113,21 +148,30 @@ export default class RoomSubList extends React.PureComponent {
|
|||
}
|
||||
|
||||
onAction = (payload) => {
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
//
|
||||
// Ultimately we need to transition to a state pushing flow where something
|
||||
// explicitly notifies the components concerned that the notif count for a room
|
||||
// has change (e.g. a Flux store).
|
||||
if (payload.action === 'on_room_read' &&
|
||||
this.props.list.some((r) => r.roomId === payload.roomId)
|
||||
) {
|
||||
this.forceUpdate();
|
||||
switch (payload.action) {
|
||||
case 'on_room_read':
|
||||
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
|
||||
// but this is no longer true, so we must do it here (and can apply the small
|
||||
// optimisation of checking that we care about the room being read).
|
||||
//
|
||||
// Ultimately we need to transition to a state pushing flow where something
|
||||
// explicitly notifies the components concerned that the notif count for a room
|
||||
// has change (e.g. a Flux store).
|
||||
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'view_room':
|
||||
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
|
||||
this.props.list.some((r) => r.roomId === payload.room_id)
|
||||
) {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
toggle = () => {
|
||||
if (this.isCollapsibleOnClick()) {
|
||||
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
|
||||
const isHidden = !this.state.hidden;
|
||||
|
@ -140,6 +184,10 @@ export default class RoomSubList extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onClick = (ev) => {
|
||||
this.toggle();
|
||||
};
|
||||
|
||||
onHeaderKeyDown = (ev) => {
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
|
@ -182,6 +230,7 @@ export default class RoomSubList extends React.PureComponent {
|
|||
onRoomTileClick = (roomId, ev) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||
});
|
||||
|
@ -195,7 +244,7 @@ export default class RoomSubList extends React.PureComponent {
|
|||
};
|
||||
|
||||
makeRoomTile = (room) => {
|
||||
return <RoomTile
|
||||
return <RoomTileErrorBoundary roomId={room.roomId}><RoomTile
|
||||
room={room}
|
||||
roomSubList={this}
|
||||
tagName={this.props.tagName}
|
||||
|
@ -208,7 +257,7 @@ export default class RoomSubList extends React.PureComponent {
|
|||
refreshSubList={this._updateSubListCount}
|
||||
incomingCall={null}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>;
|
||||
/></RoomTileErrorBoundary>;
|
||||
};
|
||||
|
||||
_onNotifBadgeClick = (e) => {
|
||||
|
@ -371,7 +420,7 @@ export default class RoomSubList extends React.PureComponent {
|
|||
|
||||
setHeight = (height) => {
|
||||
if (this._subList.current) {
|
||||
this._subList.current.style.height = `${height}px`;
|
||||
this._subList.current.style.height = toPx(height);
|
||||
}
|
||||
this._updateLazyRenderHeight(height);
|
||||
};
|
||||
|
|
|
@ -30,19 +30,18 @@ import classNames from 'classnames';
|
|||
import { _t } from '../../languageHandler';
|
||||
import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks';
|
||||
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import Modal from '../../Modal';
|
||||
import * as sdk from '../../index';
|
||||
import CallHandler from '../../CallHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import Tinter from '../../Tinter';
|
||||
import rate_limited_func from '../../ratelimitedfunc';
|
||||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch from '../../Searching';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
|
||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
|
@ -50,11 +49,13 @@ import RoomViewStore from '../../stores/RoomViewStore';
|
|||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RightPanelStore from "../../stores/RightPanelStore";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -97,8 +98,12 @@ export default createReactClass({
|
|||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||
},
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled();
|
||||
const llMembers = this.context.hasLazyLoadMembersEnabled();
|
||||
return {
|
||||
room: null,
|
||||
roomId: null,
|
||||
|
@ -131,6 +136,8 @@ export default createReactClass({
|
|||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
|
@ -158,30 +165,47 @@ export default createReactClass({
|
|||
|
||||
canReact: false,
|
||||
canReply: false,
|
||||
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.on("Room", this.onRoom);
|
||||
this.context.on("Room.timeline", this.onRoomTimeline);
|
||||
this.context.on("Room.name", this.onRoomName);
|
||||
this.context.on("Room.accountData", this.onRoomAccountData);
|
||||
this.context.on("RoomState.events", this.onRoomStateEvents);
|
||||
this.context.on("RoomState.members", this.onRoomStateMember);
|
||||
this.context.on("Room.myMembership", this.onMyMembership);
|
||||
this.context.on("accountData", this.onAccountData);
|
||||
this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||
this._onReadReceiptsChange);
|
||||
|
||||
this._roomView = createRef();
|
||||
this._searchResultsPanel = createRef();
|
||||
|
||||
this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||
},
|
||||
|
||||
_onReadReceiptsChange: function() {
|
||||
this.setState({
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||
});
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
|
@ -204,8 +228,10 @@ export default createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const newState = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomId,
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
roomLoading: RoomViewStore.isRoomLoading(),
|
||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
|
@ -213,10 +239,17 @@ export default createReactClass({
|
|||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||
// we should only peek once we have a ready client
|
||||
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
|
||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
|
||||
};
|
||||
|
||||
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
|
||||
// Stop peeking because we have joined this room now
|
||||
this.context.stopPeeking();
|
||||
}
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -231,7 +264,7 @@ export default createReactClass({
|
|||
// NB: This does assume that the roomID will not change for the lifetime of
|
||||
// the RoomView instance
|
||||
if (initial) {
|
||||
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
||||
newState.room = this.context.getRoom(newState.roomId);
|
||||
if (newState.room) {
|
||||
newState.showApps = this._shouldShowApps(newState.room);
|
||||
this._onRoomLoaded(newState.room);
|
||||
|
@ -333,7 +366,7 @@ export default createReactClass({
|
|||
peekLoading: true,
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
this.context.peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
@ -366,7 +399,7 @@ export default createReactClass({
|
|||
});
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.context.stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
}
|
||||
}
|
||||
|
@ -380,13 +413,9 @@ export default createReactClass({
|
|||
const hideWidgetDrawer = localStorage.getItem(
|
||||
room.roomId + "_hide_widget_drawer");
|
||||
|
||||
if (hideWidgetDrawer === "true") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
|
||||
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
// This is confusing, but it means to say that we default to the tray being
|
||||
// hidden unless the user clicked to open it.
|
||||
return hideWidgetDrawer === "false";
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -404,22 +433,7 @@ export default createReactClass({
|
|||
}
|
||||
this.onResize();
|
||||
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
|
||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
// We use the setTimeout to avoid racing with focus_composer.
|
||||
if (this.state.room &&
|
||||
this.state.room.getJoinedMemberCount() == 1 &&
|
||||
this.state.room.getLiveTimeline() &&
|
||||
this.state.room.getLiveTimeline().getEvents() &&
|
||||
this.state.room.getLiveTimeline().getEvents().length <= 6) {
|
||||
const inviteBox = document.getElementById("mx_SearchableEntityList_query");
|
||||
setTimeout(function() {
|
||||
if (inviteBox) {
|
||||
inviteBox.focus();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
document.addEventListener("keydown", this.onNativeKeyDown);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
|
@ -463,6 +477,10 @@ export default createReactClass({
|
|||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
}
|
||||
|
||||
if (this.state.shouldPeek) {
|
||||
this.context.stopPeeking();
|
||||
}
|
||||
|
||||
// stop tracking room changes to format permalinks
|
||||
this._stopAllPermalinkCreators();
|
||||
|
||||
|
@ -478,18 +496,19 @@ export default createReactClass({
|
|||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
if (this.context) {
|
||||
this.context.removeListener("Room", this.onRoom);
|
||||
this.context.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
this.context.removeListener("Room.name", this.onRoomName);
|
||||
this.context.removeListener("Room.accountData", this.onRoomAccountData);
|
||||
this.context.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
this.context.removeListener("Room.myMembership", this.onMyMembership);
|
||||
this.context.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
this.context.removeListener("accountData", this.onAccountData);
|
||||
this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||
this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
}
|
||||
|
||||
window.removeEventListener('beforeunload', this.onPageUnload);
|
||||
|
@ -497,21 +516,44 @@ export default createReactClass({
|
|||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
// Remove RightPanelStore listener
|
||||
if (this._rightPanelStoreToken) {
|
||||
this._rightPanelStoreToken.remove();
|
||||
}
|
||||
|
||||
WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
if (this._showReadReceiptsWatchRef) {
|
||||
SettingsStore.unwatchSetting(this._showReadReceiptsWatchRef);
|
||||
this._showReadReceiptsWatchRef = null;
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateRoomMembers.cancelPendingCall();
|
||||
|
||||
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
|
||||
// console.log("Tinter.tint from RoomView.unmount");
|
||||
// Tinter.tint(); // reset colourscheme
|
||||
|
||||
SettingsStore.unwatchSetting(this._layoutWatcherRef);
|
||||
},
|
||||
|
||||
onLayoutChange: function() {
|
||||
this.setState({
|
||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||
});
|
||||
},
|
||||
|
||||
_onRightPanelStoreUpdate: function() {
|
||||
this.setState({
|
||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||
});
|
||||
},
|
||||
|
||||
onPageUnload(event) {
|
||||
|
@ -524,8 +566,8 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||
onNativeKeyDown: function(ev) {
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
|
@ -551,12 +593,39 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
onReactKeyDown: function(ev) {
|
||||
let handled = false;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ESCAPE:
|
||||
if (!ev.altKey && !ev.ctrlKey && !ev.shiftKey && !ev.metaKey) {
|
||||
this._messagePanel.forgetReadMarker();
|
||||
this.jumpToLiveTimeline();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.PAGE_UP:
|
||||
if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) {
|
||||
this.jumpToReadMarker();
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.U.toUpperCase():
|
||||
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) {
|
||||
dis.dispatch({ action: "upload_file" })
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'after_right_panel_phase_change':
|
||||
// We don't keep state on the right panel, so just re-render to update
|
||||
this.forceUpdate();
|
||||
break;
|
||||
case 'message_send_failed':
|
||||
case 'message_sent':
|
||||
this._checkIfAlone(this.state.room);
|
||||
|
@ -568,9 +637,7 @@ export default createReactClass({
|
|||
payload.data.description || payload.data.name);
|
||||
break;
|
||||
case 'picture_snapshot':
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
ContentMessages.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context);
|
||||
break;
|
||||
case 'notifier_enabled':
|
||||
case 'upload_started':
|
||||
|
@ -614,6 +681,32 @@ export default createReactClass({
|
|||
this.onCancelSearchClick();
|
||||
}
|
||||
break;
|
||||
case 'quote':
|
||||
if (this.state.searchResults) {
|
||||
const roomId = payload.event.getRoomId();
|
||||
if (roomId === this.state.roomId) {
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
deferred_action: payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'sync_state':
|
||||
if (!this.state.matrixClientIsReady) {
|
||||
this.setState({
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
}, () => {
|
||||
// send another "initial" RVS update to trigger peeking if needed
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -643,7 +736,7 @@ export default createReactClass({
|
|||
// we'll only be showing a spinner.
|
||||
if (this.state.joining) return;
|
||||
|
||||
if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) {
|
||||
if (ev.getSender() !== this.context.credentials.userId) {
|
||||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
|
@ -699,8 +792,7 @@ export default createReactClass({
|
|||
|
||||
_loadMembersIfJoined: async function(room) {
|
||||
// lazy load members if enabled
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
if (this.context.hasLazyLoadMembersEnabled()) {
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
|
@ -735,7 +827,7 @@ export default createReactClass({
|
|||
|
||||
_updatePreviewUrlVisibility: function({roomId}) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
|
@ -768,12 +860,18 @@ export default createReactClass({
|
|||
this._updateE2EStatus(room);
|
||||
},
|
||||
|
||||
onCrossSigningKeysChanged: function() {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this._updateE2EStatus(room);
|
||||
}
|
||||
},
|
||||
|
||||
_updateE2EStatus: async function(room) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(room.roomId)) {
|
||||
if (!this.context.isRoomEncrypted(room.roomId)) {
|
||||
return;
|
||||
}
|
||||
if (!cli.isCryptoEnabled()) {
|
||||
if (!this.context.isCryptoEnabled()) {
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
|
@ -782,50 +880,10 @@ export default createReactClass({
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
|
||||
this.setState({
|
||||
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
|
||||
});
|
||||
});
|
||||
debuglog("e2e check is warning/verified only as cross-signing is off");
|
||||
return;
|
||||
}
|
||||
|
||||
// Duplication between here and _updateE2eStatus in RoomTile
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
const e2eMembers = await room.getEncryptionTargetMembers();
|
||||
const verified = [];
|
||||
const unverified = [];
|
||||
e2eMembers.map(({userId}) => userId)
|
||||
.filter((userId) => userId !== cli.getUserId())
|
||||
.forEach((userId) => {
|
||||
(cli.checkUserTrust(userId).isCrossSigningVerified() ?
|
||||
verified : unverified).push(userId)
|
||||
});
|
||||
|
||||
debuglog("e2e verified", verified, "unverified", unverified);
|
||||
|
||||
/* Check all verified user devices. */
|
||||
/* Don't alarm if no other users are verified */
|
||||
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
|
||||
for (const userId of targets) {
|
||||
const devices = await cli.getStoredDevicesForUser(userId);
|
||||
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||
});
|
||||
if (anyDeviceNotVerified) {
|
||||
this.setState({
|
||||
e2eStatus: "warning",
|
||||
});
|
||||
debuglog("e2e status set to warning as not all users trust all of their sessions." +
|
||||
" Aborted on user", userId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
e2eStatus: unverified.length === 0 ? "verified" : "normal",
|
||||
e2eStatus: await shieldStatusForRoom(this.context, room),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -894,7 +952,7 @@ export default createReactClass({
|
|||
|
||||
_updatePermissions: function(room) {
|
||||
if (room) {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
const me = this.context.getUserId();
|
||||
const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me);
|
||||
const canReply = room.maySendMessage();
|
||||
|
||||
|
@ -978,8 +1036,7 @@ export default createReactClass({
|
|||
|
||||
if (this.state.searchResults.next_batch) {
|
||||
debuglog("requesting more search results");
|
||||
const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
|
||||
this.state.searchResults);
|
||||
const searchPromise = searchPagination(this.state.searchResults);
|
||||
return this._handleSearchResult(searchPromise);
|
||||
} else {
|
||||
debuglog("no more search results");
|
||||
|
@ -1004,10 +1061,8 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onJoinButtonClicked: function(ev) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// If the user is a ROU, allow them to transition to a PWLU
|
||||
if (cli && cli.isGuest()) {
|
||||
if (this.context && this.context.isGuest()) {
|
||||
// Join this room once the user has registered and logged in
|
||||
// (If we failed to peek, we may not have a valid room object.)
|
||||
dis.dispatch({
|
||||
|
@ -1104,10 +1159,10 @@ export default createReactClass({
|
|||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(),
|
||||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||
);
|
||||
this.setState({ draggingFile: false });
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
onDragLeaveOrEnd: function(ev) {
|
||||
|
@ -1117,12 +1172,12 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
injectSticker: function(url, info, text) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (this.context.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||
ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context)
|
||||
.then(undefined, (error) => {
|
||||
if (error.name === "UnknownDeviceError") {
|
||||
// Let the staus bar handle this
|
||||
|
@ -1200,7 +1255,7 @@ export default createReactClass({
|
|||
});
|
||||
}, function(error) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Search failed: " + error);
|
||||
console.error("Search failed", error);
|
||||
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
|
||||
title: _t("Search failed"),
|
||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")),
|
||||
|
@ -1213,12 +1268,9 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
getSearchResultTiles: function() {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
// XXX: todo: merge overlapping results somehow?
|
||||
// XXX: why doesn't searching on name work?
|
||||
|
||||
|
@ -1226,21 +1278,21 @@ export default createReactClass({
|
|||
|
||||
if (this.state.searchInProgress) {
|
||||
ret.push(<li key="search-spinner">
|
||||
<Spinner />
|
||||
</li>);
|
||||
<Spinner />
|
||||
</li>);
|
||||
}
|
||||
|
||||
if (!this.state.searchResults.next_batch) {
|
||||
if (this.state.searchResults.results.length == 0) {
|
||||
ret.push(<li key="search-top-marker">
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
} else {
|
||||
ret.push(<li key="search-top-marker">
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
|
||||
</li>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1260,7 +1312,15 @@ export default createReactClass({
|
|||
|
||||
const mxEv = result.context.getEvent();
|
||||
const roomId = mxEv.getRoomId();
|
||||
const room = cli.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) {
|
||||
// if we do not have the room in js-sdk stores then hide it as we cannot easily show it
|
||||
// As per the spec, an all rooms search can create this condition,
|
||||
// it happens with Seshat but not Synapse.
|
||||
// It will make the result count not match the displayed count.
|
||||
console.log("Hiding search result from an unknown room", roomId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!haveTileForEvent(mxEv)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
|
@ -1269,16 +1329,9 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
if (this.state.searchScope === 'All') {
|
||||
if (roomId != lastRoomId) {
|
||||
|
||||
// XXX: if we've left the room, we might not know about
|
||||
// it. We should tell the js sdk to go and find out about
|
||||
// it. But that's not an issue currently, as synapse only
|
||||
// returns results for rooms we're joined to.
|
||||
const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
|
||||
|
||||
if (roomId !== lastRoomId) {
|
||||
ret.push(<li key={mxEv.getId() + "-room"}>
|
||||
<h2>{ _t("Room") }: { roomName }</h2>
|
||||
<h2>{ _t("Room") }: { room.name }</h2>
|
||||
</li>);
|
||||
lastRoomId = roomId;
|
||||
}
|
||||
|
@ -1316,7 +1369,7 @@ export default createReactClass({
|
|||
event: null,
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -1327,15 +1380,9 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onForgetClick: function() {
|
||||
MatrixClientPeg.get().forget(this.state.room.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
}, function(err) {
|
||||
const errCode = err.errcode || _t("unknown error code");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
|
||||
});
|
||||
dis.dispatch({
|
||||
action: 'forget_room',
|
||||
room_id: this.state.room.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -1344,7 +1391,7 @@ export default createReactClass({
|
|||
this.setState({
|
||||
rejecting: true,
|
||||
});
|
||||
MatrixClientPeg.get().leave(this.state.roomId).then(function() {
|
||||
this.context.leave(this.state.roomId).then(function() {
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
self.setState({
|
||||
rejecting: false,
|
||||
|
@ -1371,15 +1418,14 @@ export default createReactClass({
|
|||
rejecting: true,
|
||||
});
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
const myMember = this.state.room.getMember(cli.getUserId());
|
||||
const myMember = this.state.room.getMember(this.context.getUserId());
|
||||
const inviteEvent = myMember.events.member;
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const ignoredUsers = this.context.getIgnoredUsers();
|
||||
ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk
|
||||
await cli.setIgnoredUsers(ignoredUsers);
|
||||
await this.context.setIgnoredUsers(ignoredUsers);
|
||||
|
||||
await cli.leave(this.state.roomId);
|
||||
await this.context.leave(this.state.roomId);
|
||||
dis.dispatch({ action: 'view_next_room' });
|
||||
this.setState({
|
||||
rejecting: false,
|
||||
|
@ -1406,9 +1452,7 @@ export default createReactClass({
|
|||
// using /leave rather than /join. In the short term though, we
|
||||
// just ignore them.
|
||||
// https://github.com/vector-im/vector-web/issues/1134
|
||||
dis.dispatch({
|
||||
action: 'view_room_directory',
|
||||
});
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
},
|
||||
|
||||
onSearchClick: function() {
|
||||
|
@ -1428,7 +1472,7 @@ export default createReactClass({
|
|||
// jump down to the bottom of this room, where new events are arriving
|
||||
jumpToLiveTimeline: function() {
|
||||
this._messagePanel.jumpToLiveTimeline();
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
dis.fire(Action.FocusComposer);
|
||||
},
|
||||
|
||||
// jump up to wherever our read marker is
|
||||
|
@ -1598,7 +1642,7 @@ export default createReactClass({
|
|||
const createEvent = this.state.room.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent || !createEvent.getContent()['predecessor']) return null;
|
||||
|
||||
return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']);
|
||||
},
|
||||
|
||||
_getHiddenHighlightCount: function() {
|
||||
|
@ -1628,14 +1672,16 @@ export default createReactClass({
|
|||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
// Assume preview loading if we don't have a ready client or a room ID (still resolving the alias)
|
||||
const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
previewLoading={previewLoading && !this.state.roomLoadError}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
|
@ -1660,7 +1706,8 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_RoomView">
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
<RoomPreviewBar
|
||||
onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
|
@ -1692,10 +1739,13 @@ export default createReactClass({
|
|||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUserId = this.context.credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
const inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
const inviteEvent = myMember ? myMember.events.member : null;
|
||||
let inviterName = _t("Unknown");
|
||||
if (inviteEvent) {
|
||||
inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
}
|
||||
|
||||
// We deliberately don't try to peek into invites, even if we have permission to peek
|
||||
// as they could be a spam vector.
|
||||
|
@ -1759,13 +1809,14 @@ export default createReactClass({
|
|||
const showRoomUpgradeBar = (
|
||||
roomVersionRecommendation &&
|
||||
roomVersionRecommendation.needsUpgrade &&
|
||||
this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId)
|
||||
this.state.room.userMayUpgradeRoom(this.context.credentials.userId)
|
||||
);
|
||||
|
||||
const showRoomRecoveryReminder = (
|
||||
this.context.isCryptoEnabled() &&
|
||||
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||
MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) &&
|
||||
!MatrixClientPeg.get().getKeyBackupEnabled()
|
||||
this.context.isRoomEncrypted(this.state.room.roomId) &&
|
||||
this.context.getKeyBackupEnabled() === false
|
||||
);
|
||||
|
||||
const hiddenHighlightCount = this._getHiddenHighlightCount();
|
||||
|
@ -1836,7 +1887,7 @@ export default createReactClass({
|
|||
const auxPanel = (
|
||||
<AuxPanel room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
userId={this.context.credentials.userId}
|
||||
conferenceHandler={this.props.ConferenceHandler}
|
||||
draggingFile={this.state.draggingFile}
|
||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||
|
@ -1876,25 +1927,29 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
if (inCall) {
|
||||
let zoomButton; let voiceMuteButton; let videoMuteButton;
|
||||
let zoomButton; let videoMuteButton;
|
||||
|
||||
if (call.type === "video") {
|
||||
zoomButton = (
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
|
||||
<TintableSvg src={require("../../../res/img/fullscreen.svg")} width="29" height="22" style={{ marginTop: 1, marginRight: 4 }} />
|
||||
<TintableSvg src={require("../../../res/img/element-icons/call/fullscreen.svg")} width="29" height="22" style={{ marginTop: 1, marginRight: 4 }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
videoMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
||||
<TintableSvg src={call.isLocalVideoMuted() ? require("../../../res/img/video-unmute.svg") : require("../../../res/img/video-mute.svg")}
|
||||
<TintableSvg src={call.isLocalVideoMuted() ?
|
||||
require("../../../res/img/element-icons/call/video-muted.svg") :
|
||||
require("../../../res/img/element-icons/call/video-call.svg")}
|
||||
alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
|
||||
width="31" height="27" />
|
||||
width="" height="27" />
|
||||
</div>;
|
||||
}
|
||||
voiceMuteButton =
|
||||
const voiceMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||
<TintableSvg src={call.isMicrophoneMuted() ? require("../../../res/img/voice-unmute.svg") : require("../../../res/img/voice-mute.svg")}
|
||||
<TintableSvg src={call.isMicrophoneMuted() ?
|
||||
require("../../../res/img/element-icons/call/voice-muted.svg") :
|
||||
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
|
||||
alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
|
||||
width="21" height="26" />
|
||||
</div>;
|
||||
|
@ -1906,7 +1961,6 @@ export default createReactClass({
|
|||
{ videoMuteButton }
|
||||
{ zoomButton }
|
||||
{ statusBar }
|
||||
<TintableSvg className="mx_RoomView_voipChevron" src={require("../../../res/img/voip-chevron.svg")} width="22" height="17" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -1921,8 +1975,9 @@ export default createReactClass({
|
|||
searchResultsPanel = (<div className="mx_RoomView_messagePanel mx_RoomView_messagePanelSearchSpinner" />);
|
||||
} else {
|
||||
searchResultsPanel = (
|
||||
<ScrollPanel ref={this._searchResultsPanel}
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
|
||||
<ScrollPanel
|
||||
ref={this._searchResultsPanel}
|
||||
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel mx_GroupLayout"
|
||||
onFillRequest={this.onSearchResultsFillRequest}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
|
@ -1942,12 +1997,19 @@ export default createReactClass({
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
const messagePanelClassNames = classNames(
|
||||
"mx_RoomView_messagePanel",
|
||||
{
|
||||
"mx_IRCLayout": this.state.useIRCLayout,
|
||||
"mx_GroupLayout": !this.state.useIRCLayout,
|
||||
});
|
||||
|
||||
// console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
|
||||
const messagePanel = (
|
||||
<TimelinePanel
|
||||
ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
showReadReceipts={SettingsStore.getValue('showReadReceipts')}
|
||||
showReadReceipts={this.state.showReadReceipts}
|
||||
manageReadReceipts={!this.state.isPeeking}
|
||||
manageReadMarkers={!this.state.isPeeking}
|
||||
hidden={hideMessagePanel}
|
||||
|
@ -1957,11 +2019,12 @@ export default createReactClass({
|
|||
onScroll={this.onMessageListScroll}
|
||||
onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
|
||||
showUrlPreview = {this.state.showUrlPreview}
|
||||
className="mx_RoomView_messagePanel"
|
||||
className={messagePanelClassNames}
|
||||
membersLoaded={this.state.membersLoaded}
|
||||
permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showReactions={true}
|
||||
useIRCLayout={this.state.useIRCLayout}
|
||||
/>);
|
||||
|
||||
let topUnreadMessagesBar = null;
|
||||
|
@ -1978,6 +2041,7 @@ export default createReactClass({
|
|||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
/>);
|
||||
|
@ -1996,15 +2060,22 @@ export default createReactClass({
|
|||
},
|
||||
);
|
||||
|
||||
const showRightPanel = !forceHideRightPanel && this.state.room
|
||||
&& RightPanelStore.getSharedInstance().isOpenForRoom;
|
||||
const showRightPanel = !forceHideRightPanel && this.state.room && this.state.showRightPanel;
|
||||
const rightPanel = showRightPanel
|
||||
? <RightPanel roomId={this.state.room.roomId} resizeNotifier={this.props.resizeNotifier} />
|
||||
: null;
|
||||
|
||||
const timelineClasses = classNames("mx_RoomView_timeline", {
|
||||
mx_RoomView_timeline_rr_enabled: this.state.showReadReceipts,
|
||||
});
|
||||
|
||||
const mainClasses = classNames("mx_RoomView", {
|
||||
mx_RoomView_inCall: inCall,
|
||||
});
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref={this._roomView}>
|
||||
<main className={mainClasses} ref={this._roomView} onKeyDown={this.onReactKeyDown}>
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
|
@ -2025,7 +2096,7 @@ export default createReactClass({
|
|||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
<div className={timelineClasses}>
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
|
|
|
@ -144,6 +144,11 @@ export default createReactClass({
|
|||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren: PropTypes.node,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -156,9 +161,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._fillRequestWhileRunning = false;
|
||||
this._isFilling = false;
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._pendingFillRequests = {b: null, f: null};
|
||||
|
||||
if (this.props.resizeNotifier) {
|
||||
|
@ -523,7 +527,7 @@ export default createReactClass({
|
|||
scrollRelative: function(mult) {
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
scrollNode.scrollTop = scrollNode.scrollTop + delta;
|
||||
scrollNode.scrollBy(0, delta);
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
|
@ -705,17 +709,15 @@ export default createReactClass({
|
|||
// the currently filled piece of the timeline
|
||||
if (trackedNode) {
|
||||
const oldTop = trackedNode.offsetTop;
|
||||
// changing the height might change the scrollTop
|
||||
// if the new height is smaller than the scrollTop.
|
||||
// We calculate the diff that needs to be applied
|
||||
// ourselves, so be sure to measure the
|
||||
// scrollTop before changing the height.
|
||||
const preexistingScrollTop = sn.scrollTop;
|
||||
itemlist.style.height = `${newHeight}px`;
|
||||
const newTop = trackedNode.offsetTop;
|
||||
const topDiff = newTop - oldTop;
|
||||
sn.scrollTop = preexistingScrollTop + topDiff;
|
||||
debuglog("updateHeight to", {newHeight, topDiff, preexistingScrollTop});
|
||||
// important to scroll by a relative amount as
|
||||
// reading scrollTop and then setting it might
|
||||
// yield out of date values and cause a jump
|
||||
// when setting it
|
||||
sn.scrollBy(0, topDiff);
|
||||
debuglog("updateHeight to", {newHeight, topDiff});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -767,6 +769,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
// current capped height - distance from top = distance from bottom of container to top of tracked element
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
|
@ -783,7 +786,7 @@ export default createReactClass({
|
|||
if (!this._divScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before gemini ref collected");
|
||||
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
|
||||
}
|
||||
|
||||
return this._divScroll;
|
||||
|
@ -883,6 +886,7 @@ export default createReactClass({
|
|||
return (<AutoHideScrollbar wrappedRef={this._collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
|
||||
{ this.props.fixedChildren }
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
|
||||
{ this.props.children }
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, {createRef} from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Key } from '../../Keyboard';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
@ -53,6 +53,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._search = createRef();
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,8 +18,10 @@ limitations under the License.
|
|||
|
||||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import * as PropTypes from "prop-types";
|
||||
import * as sdk from "../../index";
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
|
@ -27,32 +29,40 @@ import * as sdk from "../../index";
|
|||
export class Tab {
|
||||
/**
|
||||
* Creates a new tab.
|
||||
* @param {string} tabLabel The untranslated tab label.
|
||||
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
|
||||
* @param {string} tabJsx The JSX for the tab container.
|
||||
* @param {string} id The tab's ID.
|
||||
* @param {string} label The untranslated tab label.
|
||||
* @param {string} icon The class for the tab icon. This should be a simple mask.
|
||||
* @param {React.ReactNode} body The JSX for the tab container.
|
||||
*/
|
||||
constructor(tabLabel, tabIconClass, tabJsx) {
|
||||
this.label = tabLabel;
|
||||
this.icon = tabIconClass;
|
||||
this.body = tabJsx;
|
||||
constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) {
|
||||
}
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component {
|
||||
static propTypes = {
|
||||
// The tabs to show
|
||||
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
|
||||
};
|
||||
interface IProps {
|
||||
tabs: Tab[];
|
||||
initialTabId?: string;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
interface IState {
|
||||
activeTabIndex: number;
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let activeTabIndex = 0;
|
||||
if (props.initialTabId) {
|
||||
const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId);
|
||||
if (tabIndex >= 0) activeTabIndex = tabIndex;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
activeTabIndex: 0,
|
||||
activeTabIndex,
|
||||
};
|
||||
}
|
||||
|
||||
_getActiveTabIndex() {
|
||||
private _getActiveTabIndex() {
|
||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||
return this.state.activeTabIndex;
|
||||
}
|
||||
|
@ -62,7 +72,7 @@ export default class TabbedView extends React.Component {
|
|||
* @param {Tab} tab the tab to show
|
||||
* @private
|
||||
*/
|
||||
_setActiveTab(tab) {
|
||||
private _setActiveTab(tab: Tab) {
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx !== -1) {
|
||||
this.setState({activeTabIndex: idx});
|
||||
|
@ -71,7 +81,7 @@ export default class TabbedView extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_renderTabLabel(tab) {
|
||||
private _renderTabLabel(tab: Tab) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let classes = "mx_TabbedView_tabLabel ";
|
||||
|
@ -97,17 +107,17 @@ export default class TabbedView extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderTabPanel(tab) {
|
||||
private _renderTabPanel(tab: Tab): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||
<div className='mx_TabbedView_tabPanelContent'>
|
||||
<AutoHideScrollbar className='mx_TabbedView_tabPanelContent'>
|
||||
{tab.body}
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||
|
|
@ -22,12 +22,13 @@ import TagOrderStore from '../../stores/TagOrderStore';
|
|||
import GroupActions from '../../actions/GroupActions';
|
||||
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
||||
const TagPanel = createReactClass({
|
||||
displayName: 'TagPanel',
|
||||
|
@ -43,7 +44,7 @@ const TagPanel = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.on("sync", this._onClientSync);
|
||||
|
@ -106,7 +107,6 @@ const TagPanel = createReactClass({
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
|
@ -138,9 +138,8 @@ const TagPanel = createReactClass({
|
|||
{ clearButton }
|
||||
</div>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
<AutoHideScrollbar
|
||||
className="mx_TagPanel_scroller"
|
||||
autoshow={true}
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
|
@ -166,7 +165,7 @@ const TagPanel = createReactClass({
|
|||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbarWrapper>
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
const TagPanelButtons = createReactClass({
|
||||
displayName: 'TagPanelButtons',
|
||||
|
||||
|
||||
componentDidMount: function() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
this._dispatcherRef = null;
|
||||
}
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === "show_redesign_feedback_dialog") {
|
||||
const RedesignFeedbackDialog =
|
||||
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const ActionButton = sdk.getComponent("elements.ActionButton");
|
||||
|
||||
return (<div className="mx_TagPanelButtons">
|
||||
<GroupsButton />
|
||||
<ActionButton
|
||||
className="mx_TagPanelButtons_report" action="show_redesign_feedback_dialog"
|
||||
label={_t("Report bugs & give feedback")} tooltip={true} />
|
||||
</div>);
|
||||
},
|
||||
});
|
||||
export default TagPanelButtons;
|
|
@ -29,7 +29,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
|
|||
import * as ObjectUtils from "../../ObjectUtils";
|
||||
import UserActivity from "../../UserActivity";
|
||||
import Modal from "../../Modal";
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../index";
|
||||
import { Key } from '../../Keyboard';
|
||||
import Timer from '../../utils/Timer';
|
||||
|
@ -112,6 +112,9 @@ const TimelinePanel = createReactClass({
|
|||
|
||||
// whether to show reactions for an event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -202,7 +205,8 @@ const TimelinePanel = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
debuglog("TimelinePanel: mounting");
|
||||
|
||||
this.lastRRSentEventId = undefined;
|
||||
|
@ -234,7 +238,8 @@ const TimelinePanel = createReactClass({
|
|||
this._initTimeline(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.timelineSet !== this.props.timelineSet) {
|
||||
// throw new Error("changing timelineSet on a TimelinePanel is not supported");
|
||||
|
||||
|
@ -793,6 +798,9 @@ const TimelinePanel = createReactClass({
|
|||
readMarkerVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Send the updated read marker (along with read receipt) to the server
|
||||
this.sendReadReceipt();
|
||||
},
|
||||
|
||||
|
||||
|
@ -1445,6 +1453,7 @@ const TimelinePanel = createReactClass({
|
|||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
editState={this.state.editState}
|
||||
showReactions={this.props.showReactions}
|
||||
useIRCLayout={this.props.useIRCLayout}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,14 +15,21 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import ToastStore, {IToast} from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
export default class ToastContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {toasts: ToastStore.sharedInstance().getToasts()};
|
||||
interface IState {
|
||||
toasts: IToast<any>[];
|
||||
countSeen: number;
|
||||
}
|
||||
|
||||
export default class ToastContainer extends React.Component<{}, IState> {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
};
|
||||
|
||||
// Start listening here rather than in componentDidMount because
|
||||
// toasts may dismiss themselves in their didMount if they find
|
||||
|
@ -36,7 +43,10 @@ export default class ToastContainer extends React.Component {
|
|||
}
|
||||
|
||||
_onToastStoreUpdate = () => {
|
||||
this.setState({toasts: ToastStore.sharedInstance().getToasts()});
|
||||
this.setState({
|
||||
toasts: ToastStore.sharedInstance().getToasts(),
|
||||
countSeen: ToastStore.sharedInstance().getCountSeen(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -50,14 +60,21 @@ export default class ToastContainer extends React.Component {
|
|||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
countIndicator = ` (${this.state.countSeen + 1}/${this.state.countSeen + totalCount})`;
|
||||
}
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
key,
|
||||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{title}</h2>
|
||||
<span>{countIndicator}</span>
|
||||
</div>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
|
@ -22,8 +22,9 @@ import BaseAvatar from '../views/avatars/BaseAvatar';
|
|||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import * as Avatar from '../../Avatar';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
|
||||
const AVATAR_SIZE = 28;
|
||||
|
||||
|
@ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component {
|
|||
|
||||
onAction = (payload) => {
|
||||
// For accessibility
|
||||
if (payload.action === "toggle_top_left_menu") {
|
||||
if (payload.action === Action.ToggleUserMenu) {
|
||||
if (this._buttonRef) this._buttonRef.click();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import ContentMessages from '../../ContentMessages';
|
||||
import dis from "../../dispatcher";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import filesize from "filesize";
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
|
|
359
src/components/structures/UserMenu.tsx
Normal file
359
src/components/structures/UserMenu.tsx
Normal file
|
@ -0,0 +1,359 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
|
||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import {getCustomTheme} from "../../theme";
|
||||
import {getHostingLink} from "../../utils/HostingLink";
|
||||
import {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||
import classNames from "classnames";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "width" | "left" | "top" | "height">;
|
||||
|
||||
interface IState {
|
||||
contextMenuPosition: PartialDOMRect;
|
||||
isDarkTheme: boolean;
|
||||
}
|
||||
|
||||
interface IMenuButtonProps {
|
||||
iconClassName: string;
|
||||
label: string;
|
||||
onClick(ev: ButtonEvent);
|
||||
}
|
||||
|
||||
const MenuButton: React.FC<IMenuButtonProps> = ({iconClassName, label, onClick}) => {
|
||||
return <MenuItem label={label} onClick={onClick}>
|
||||
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
|
||||
<span className="mx_IconizedContextMenu_label">{label}</span>
|
||||
</MenuItem>;
|
||||
};
|
||||
|
||||
export default class UserMenu extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contextMenuPosition: null,
|
||||
isDarkTheme: this.isUserOnDarkTheme(),
|
||||
};
|
||||
|
||||
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private get hasHomePage(): boolean {
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme.startsWith("custom-")) {
|
||||
return getCustomTheme(theme.substring("custom-".length)).is_dark;
|
||||
}
|
||||
return theme === "dark";
|
||||
}
|
||||
|
||||
private onProfileUpdate = async () => {
|
||||
// the store triggered an update, so force a layout update. We don't
|
||||
// have any state to store here for that to magically happen.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private onThemeChanged = () => {
|
||||
this.setState({isDarkTheme: this.isUserOnDarkTheme()});
|
||||
};
|
||||
|
||||
private onAction = (ev: ActionPayload) => {
|
||||
if (ev.action !== Action.ToggleUserMenu) return; // not interested
|
||||
|
||||
if (this.state.contextMenuPosition) {
|
||||
this.setState({contextMenuPosition: null});
|
||||
} else {
|
||||
if (this.buttonRef.current) this.buttonRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({contextMenuPosition: target.getBoundingClientRect()});
|
||||
};
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
contextMenuPosition: {
|
||||
left: ev.clientX,
|
||||
top: ev.clientY,
|
||||
width: 20,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private onCloseMenu = () => {
|
||||
this.setState({contextMenuPosition: null});
|
||||
};
|
||||
|
||||
private onSwitchThemeClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Disable system theme matching if the user hits this button
|
||||
SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||
|
||||
const newTheme = this.state.isDarkTheme ? "light" : "dark";
|
||||
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
|
||||
};
|
||||
|
||||
private onSettingsOpen = (ev: ButtonEvent, tabId: string) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
|
||||
defaultDispatcher.dispatch(payload);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onShowArchived = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038
|
||||
// Note: You'll need to uncomment the button too.
|
||||
console.log("TODO: Show archived rooms");
|
||||
};
|
||||
|
||||
private onProvideFeedback = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.contextMenuPosition) return null;
|
||||
|
||||
let hostingLink;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
hostingLink = (
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => (
|
||||
<a
|
||||
href={signupLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
tabIndex={-1}
|
||||
>{sub}</a>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let homeButton = null;
|
||||
if (this.hasHomePage) {
|
||||
homeButton = (
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconHome"
|
||||
label={_t("Home")}
|
||||
onClick={this.onHomeClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
chevronFace={ChevronFace.None}
|
||||
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
>
|
||||
<div className="mx_IconizedContextMenu mx_UserMenu_contextMenu">
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
</span>
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||
>
|
||||
<img
|
||||
src={require("../../../res/img/element-icons/roomlist/dark-light-mode.svg")}
|
||||
alt={_t("Switch theme")}
|
||||
width={16}
|
||||
/>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{hostingLink}
|
||||
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||
{homeButton}
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
||||
/>
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
||||
/>
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{/* <MenuButton
|
||||
iconClassName="mx_UserMenu_iconArchive"
|
||||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */}
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_IconizedContextMenu_optionList mx_UserMenu_contextMenu_redRow">
|
||||
<MenuButton
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const avatarSize = 32; // should match border-radius of the avatar
|
||||
const {body} = document;
|
||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||
body.style.setProperty("--avatar-url", `url('${avatarUrl}')`);
|
||||
|
||||
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuButton
|
||||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("User menu")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={MatrixClientPeg.get().getUserId()}
|
||||
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
||||
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
resizeMethod="crop"
|
||||
className="mx_UserMenu_userAvatar"
|
||||
/>
|
||||
</span>
|
||||
{name}
|
||||
{buttons}
|
||||
</div>
|
||||
</ContextMenuButton>
|
||||
{this.renderContextMenu()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg";
|
|||
import * as sdk from "../../index";
|
||||
import Modal from '../../Modal';
|
||||
import { _t } from '../../languageHandler';
|
||||
import HomePage from "./HomePage";
|
||||
|
||||
export default class UserView extends React.Component {
|
||||
static get propTypes() {
|
||||
|
@ -35,14 +36,17 @@ export default class UserView extends React.Component {
|
|||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.userId) {
|
||||
this._loadProfileInfo();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.userId !== this.props.userId) {
|
||||
// XXX: We shouldn't need to null check the userId here, but we declare
|
||||
// it as optional and MatrixChat sometimes fires in a way which results
|
||||
// in an NPE when we try to update the profile info.
|
||||
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
||||
this._loadProfileInfo();
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +80,7 @@ export default class UserView extends React.Component {
|
|||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
||||
const panel = <RightPanel user={this.state.member} />;
|
||||
return (<MainSplit panel={panel}><div style={{flex: "1"}} /></MainSplit>);
|
||||
return (<MainSplit panel={panel}><HomePage /></MainSplit>);
|
||||
} else {
|
||||
return (<div />);
|
||||
}
|
||||
|
|
|
@ -18,13 +18,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
|
||||
|
||||
const PHASE_INTRO = 0;
|
||||
const PHASE_BUSY = 1;
|
||||
const PHASE_DONE = 2;
|
||||
const PHASE_CONFIRM_SKIP = 3;
|
||||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_INTRO,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
} from '../../../stores/SetupEncryptionStore';
|
||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||
|
||||
export default class CompleteSecurity extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -33,202 +34,42 @@ export default class CompleteSecurity extends React.Component {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
phase: PHASE_INTRO,
|
||||
// this serves dual purpose as the object for the request logic and
|
||||
// the presence of it insidicating that we're in 'verify mode'.
|
||||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: null,
|
||||
backupInfo: null,
|
||||
};
|
||||
MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest);
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this._onStoreUpdate);
|
||||
store.start();
|
||||
this.state = {phase: store.phase};
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
this.setState({phase: store.phase});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.verificationRequest) {
|
||||
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||
}
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
|
||||
}
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_BUSY,
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
});
|
||||
|
||||
if (cli.getCrossSigningId()) {
|
||||
this.setState({
|
||||
phase: PHASE_DONE,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof AccessCancelledError)) {
|
||||
console.log(e);
|
||||
}
|
||||
// this will throw if the user hits cancel, so ignore
|
||||
this.setState({
|
||||
phase: PHASE_INTRO,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onVerificationRequest = async (request) => {
|
||||
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||
}
|
||||
await request.accept();
|
||||
request.on("change", this.onVerificationRequestChange);
|
||||
this.setState({
|
||||
verificationRequest: request,
|
||||
});
|
||||
}
|
||||
|
||||
onVerificationRequestChange = () => {
|
||||
if (this.state.verificationRequest.cancelled) {
|
||||
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||
this.setState({
|
||||
verificationRequest: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onSkipClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_CONFIRM_SKIP,
|
||||
});
|
||||
}
|
||||
|
||||
onSkipConfirmClick = () => {
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
onSkipBackClick = () => {
|
||||
this.setState({
|
||||
phase: PHASE_INTRO,
|
||||
});
|
||||
}
|
||||
|
||||
onDoneClick = () => {
|
||||
this.props.onFinished();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.off("update", this._onStoreUpdate);
|
||||
store.stop();
|
||||
}
|
||||
|
||||
render() {
|
||||
const AuthPage = sdk.getComponent("auth.AuthPage");
|
||||
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {
|
||||
phase,
|
||||
} = this.state;
|
||||
|
||||
const {phase} = this.state;
|
||||
let icon;
|
||||
let title;
|
||||
let body;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
body = <EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (phase === PHASE_INTRO) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else if (phase === PHASE_DONE) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified"></span>;
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||
title = _t("Session verified");
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
message = <p>{_t(
|
||||
"Your new session is now verified. It has access to your " +
|
||||
"encrypted messages, and other users will see it as trusted.",
|
||||
)}</p>;
|
||||
} else {
|
||||
message = <p>{_t(
|
||||
"Your new session is now verified. Other users will see it as trusted.",
|
||||
)}</p>;
|
||||
}
|
||||
body = (
|
||||
<div>
|
||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified"></div>
|
||||
{message}
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onDoneClick}
|
||||
>
|
||||
{_t("Done")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Are you sure?");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Without completing security on this session, it won’t have " +
|
||||
"access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{_t("Go Back")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_BUSY) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = <Spinner />;
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||
title = _t("Verify this login");
|
||||
} else {
|
||||
throw new Error(`Unknown phase ${phase}`);
|
||||
}
|
||||
|
@ -241,7 +82,7 @@ export default class CompleteSecurity extends React.Component {
|
|||
{title}
|
||||
</h2>
|
||||
<div className="mx_CompleteSecurity_body">
|
||||
{body}
|
||||
<SetupEncryptionBody onFinished={this.props.onFinished} />
|
||||
</div>
|
||||
</CompleteSecurityBody>
|
||||
</AuthPage>
|
||||
|
|
|
@ -69,12 +69,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.reset = null;
|
||||
this._checkServerLiveliness(this.props.serverConfig);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
|
@ -296,7 +297,6 @@ export default createReactClass({
|
|||
<form onSubmit={this.onSubmitForm}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
id="mx_ForgotPassword_email"
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
type="text"
|
||||
label={_t('Email')}
|
||||
|
@ -307,7 +307,6 @@ export default createReactClass({
|
|||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
id="mx_ForgotPassword_password"
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_t('Password')}
|
||||
|
@ -315,7 +314,6 @@ export default createReactClass({
|
|||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
/>
|
||||
<Field
|
||||
id="mx_ForgotPassword_passwordConfirm"
|
||||
name="reset_password_confirm"
|
||||
type="password"
|
||||
label={_t('Confirm')}
|
||||
|
|
|
@ -27,6 +27,8 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
|||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
@ -82,11 +84,13 @@ export default createReactClass({
|
|||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
isSyncing: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
busy: false,
|
||||
busyLoggingIn: null,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
canTryLogin: true, // can we attempt to log in or are there validation errors?
|
||||
|
@ -111,7 +115,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
// map from login step type to a function which will render a control
|
||||
|
@ -120,8 +125,8 @@ export default createReactClass({
|
|||
'm.login.password': this._renderPasswordStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
|
||||
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
|
||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
|
@ -131,7 +136,8 @@ export default createReactClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
|
@ -165,6 +171,7 @@ export default createReactClass({
|
|||
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
busyLoggingIn: false,
|
||||
...componentState,
|
||||
});
|
||||
aliveAgain = !componentState.serverErrorIsFatal;
|
||||
|
@ -178,6 +185,7 @@ export default createReactClass({
|
|||
|
||||
this.setState({
|
||||
busy: true,
|
||||
busyLoggingIn: true,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
@ -245,6 +253,8 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
this.setState({
|
||||
busy: false,
|
||||
busyLoggingIn: false,
|
||||
errorText: errorText,
|
||||
// 401 would be the sensible status code for 'incorrect password'
|
||||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
|
@ -252,13 +262,6 @@ export default createReactClass({
|
|||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -344,6 +347,22 @@ export default createReactClass({
|
|||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onTryRegisterClick: function(ev) {
|
||||
const step = this._getCurrentFlowStep();
|
||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||
// If we're showing SSO it means that registration is also probably disabled,
|
||||
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
||||
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind,
|
||||
this.props.fragmentAfterLogin);
|
||||
} else {
|
||||
// Don't intercept - just go through to the register page
|
||||
this.onRegisterClick(ev);
|
||||
}
|
||||
},
|
||||
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
|
@ -581,11 +600,12 @@ export default createReactClass({
|
|||
loginIncorrect={this.state.loginIncorrect}
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
busy={this.props.isSyncing || this.state.busyLoggingIn}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
_renderSsoStep: function(url) {
|
||||
_renderSsoStep: function(loginType) {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let onEditServerDetailsClick = null;
|
||||
|
@ -606,16 +626,23 @@ export default createReactClass({
|
|||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
||||
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
const loader = this.isBusy() ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
const loader = this.isBusy() && !this.state.busyLoggingIn ?
|
||||
<div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
const errorText = this.state.errorText;
|
||||
|
||||
|
@ -642,9 +669,28 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let footer;
|
||||
if (this.props.isSyncing || this.state.busyLoggingIn) {
|
||||
footer = <div className="mx_AuthBody_paddedFooter">
|
||||
<div className="mx_AuthBody_paddedFooter_title">
|
||||
<InlineSpinner w={20} h={20} />
|
||||
{ this.props.isSyncing ? _t("Syncing...") : _t("Signing In...") }
|
||||
</div>
|
||||
{ this.props.isSyncing && <div className="mx_AuthBody_paddedFooter_subtitle">
|
||||
{_t("If you've joined lots of rooms, this might take a while")}
|
||||
</div> }
|
||||
</div>;
|
||||
} else {
|
||||
footer = (
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthPage>
|
||||
<AuthHeader />
|
||||
<AuthHeader disableLanguageSelector={this.props.isSyncing || this.state.busyLoggingIn} />
|
||||
<AuthBody>
|
||||
<h2>
|
||||
{_t('Sign in')}
|
||||
|
@ -654,9 +700,7 @@ export default createReactClass({
|
|||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
{ footer }
|
||||
</AuthBody>
|
||||
</AuthPage>
|
||||
);
|
||||
|
|
|
@ -37,7 +37,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||
// the URL to be passed to you (because it's also used for room avatars).
|
||||
|
|
|
@ -31,6 +31,8 @@ import classNames from "classnames";
|
|||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login from "../../../Login";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -118,12 +120,13 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._replaceClient();
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||
|
||||
|
@ -232,29 +235,55 @@ export default createReactClass({
|
|||
serverRequiresIdServer,
|
||||
busy: false,
|
||||
});
|
||||
const showGenericError = (e) => {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
};
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
// auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
// We do the first registration request ourselves to discover whether we need to
|
||||
// do SSO instead. If we've already started the UI Auth process though, we don't
|
||||
// need to.
|
||||
if (!this.state.doingUIAuth) {
|
||||
await this._makeRegisterRequest(null);
|
||||
// This should never succeed since we specified no auth object.
|
||||
console.log("Expecting 401 from register request but got success!");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 401) {
|
||||
this.setState({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
try {
|
||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used
|
||||
});
|
||||
const flows = await loginLogic.getFlows();
|
||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
||||
if (hasSsoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
serverErrorIsFatal: true, // fatal because user cannot continue on this server
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
showGenericError(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
showGenericError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -349,7 +378,7 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
if (response.access_token) {
|
||||
const cli = await this.props.onLoggedIn({
|
||||
await this.props.onLoggedIn({
|
||||
userId: response.user_id,
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this.state.matrixClient.getHomeserverUrl(),
|
||||
|
@ -357,7 +386,7 @@ export default createReactClass({
|
|||
accessToken: response.access_token,
|
||||
}, this.state.formVals.password);
|
||||
|
||||
this._setupPushers(cli);
|
||||
this._setupPushers();
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
newState.busy = true;
|
||||
} else {
|
||||
|
@ -368,10 +397,11 @@ export default createReactClass({
|
|||
this.setState(newState);
|
||||
},
|
||||
|
||||
_setupPushers: function(matrixClient) {
|
||||
_setupPushers: function() {
|
||||
if (!this.props.brand) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
return matrixClient.getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
|
@ -439,7 +469,7 @@ export default createReactClass({
|
|||
initial_device_display_name: this.props.defaultDeviceDisplayName,
|
||||
};
|
||||
if (auth) registerParams.auth = auth;
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibitLogin = inhibitLogin;
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
|
||||
return this.state.matrixClient.registerRequest(registerParams);
|
||||
},
|
||||
|
||||
|
|
225
src/components/structures/auth/SetupEncryptionBody.js
Normal file
225
src/components/structures/auth/SetupEncryptionBody.js
Normal file
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import {
|
||||
SetupEncryptionStore,
|
||||
PHASE_INTRO,
|
||||
PHASE_BUSY,
|
||||
PHASE_DONE,
|
||||
PHASE_CONFIRM_SKIP,
|
||||
PHASE_FINISHED,
|
||||
} from '../../../stores/SetupEncryptionStore';
|
||||
|
||||
function keyHasPassphrase(keyInfo) {
|
||||
return (
|
||||
keyInfo.passphrase &&
|
||||
keyInfo.passphrase.salt &&
|
||||
keyInfo.passphrase.iterations
|
||||
);
|
||||
}
|
||||
|
||||
export default class SetupEncryptionBody extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.on("update", this._onStoreUpdate);
|
||||
store.start();
|
||||
this.state = {
|
||||
phase: store.phase,
|
||||
// this serves dual purpose as the object for the request logic and
|
||||
// the presence of it indicating that we're in 'verify mode'.
|
||||
// Because of the latter, it lives in the state.
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
};
|
||||
}
|
||||
|
||||
_onStoreUpdate = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
if (store.phase === PHASE_FINISHED) {
|
||||
this.props.onFinished();
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
phase: store.phase,
|
||||
verificationRequest: store.verificationRequest,
|
||||
backupInfo: store.backupInfo,
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.off("update", this._onStoreUpdate);
|
||||
store.stop();
|
||||
}
|
||||
|
||||
_onUsePassphraseClick = async () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.usePassPhrase();
|
||||
}
|
||||
|
||||
onSkipClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skip();
|
||||
}
|
||||
|
||||
onSkipConfirmClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.skipConfirm();
|
||||
}
|
||||
|
||||
onSkipBackClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.returnAfterSkip();
|
||||
}
|
||||
|
||||
onDoneClick = () => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.done();
|
||||
}
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const {
|
||||
phase,
|
||||
} = this.state;
|
||||
|
||||
if (this.state.verificationRequest) {
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
return <EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
let recoveryKeyPrompt;
|
||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key or Passphrase");
|
||||
} else if (store.keyInfo) {
|
||||
recoveryKeyPrompt = _t("Use Recovery Key");
|
||||
}
|
||||
|
||||
let useRecoveryKeyButton;
|
||||
if (recoveryKeyPrompt) {
|
||||
useRecoveryKeyButton = <AccessibleButton kind="link" onClick={this._onUsePassphraseClick}>
|
||||
{recoveryKeyPrompt}
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Confirm your identity by verifying this login from one of your other sessions, " +
|
||||
"granting it access to encrypted messages.",
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"This requires the latest %(brand)s on your other devices:",
|
||||
{ brand },
|
||||
)}</p>
|
||||
|
||||
<div className="mx_CompleteSecurity_clients">
|
||||
<div className="mx_CompleteSecurity_clients_desktop">
|
||||
<div>{_t("%(brand)s Web", { brand })}</div>
|
||||
<div>{_t("%(brand)s Desktop", { brand })}</div>
|
||||
</div>
|
||||
<div className="mx_CompleteSecurity_clients_mobile">
|
||||
<div>{_t("%(brand)s iOS", { brand })}</div>
|
||||
<div>{_t("%(brand)s X for Android", { brand })}</div>
|
||||
</div>
|
||||
<p>{_t("or another cross-signing capable Matrix client")}</p>
|
||||
</div>
|
||||
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
{useRecoveryKeyButton}
|
||||
<AccessibleButton kind="danger" onClick={this.onSkipClick}>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_DONE) {
|
||||
let message;
|
||||
if (this.state.backupInfo) {
|
||||
message = <p>{_t(
|
||||
"Your new session is now verified. It has access to your " +
|
||||
"encrypted messages, and other users will see it as trusted.",
|
||||
)}</p>;
|
||||
} else {
|
||||
message = <p>{_t(
|
||||
"Your new session is now verified. Other users will see it as trusted.",
|
||||
)}</p>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_CompleteSecurity_heroIcon mx_E2EIcon_verified" />
|
||||
{message}
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onDoneClick}
|
||||
>
|
||||
{_t("Done")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Without completing security on this session, it won’t have " +
|
||||
"access to encrypted messages.",
|
||||
)}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
className="warning"
|
||||
kind="secondary"
|
||||
onClick={this.onSkipConfirmClick}
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
onClick={this.onSkipBackClick}
|
||||
>
|
||||
{_t("Go Back")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (phase === PHASE_BUSY) {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <Spinner />;
|
||||
} else {
|
||||
console.log(`SetupEncryptionBody: Unknown phase ${phase}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,13 +18,14 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import url from 'url';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
|
@ -43,7 +44,7 @@ const FLOWS_TO_VIEWS = {
|
|||
export default class SoftLogout extends React.Component {
|
||||
static propTypes = {
|
||||
// Query parameters from MatrixChat
|
||||
realQueryParams: PropTypes.object, // {homeserver, identityServer, loginToken}
|
||||
realQueryParams: PropTypes.object, // {loginToken}
|
||||
|
||||
// Called when the SSO login completes
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
|
@ -54,8 +55,7 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
this.state = {
|
||||
loginView: LOGIN_VIEW.LOADING,
|
||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
|
||||
ssoUrl: null,
|
||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount)
|
||||
|
||||
busy: false,
|
||||
password: "",
|
||||
|
@ -91,7 +91,7 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
async _initLogin() {
|
||||
const queryParams = this.props.realQueryParams;
|
||||
const hasAllParams = queryParams && queryParams['homeserver'] && queryParams['loginToken'];
|
||||
const hasAllParams = queryParams && queryParams['loginToken'];
|
||||
if (hasAllParams) {
|
||||
this.setState({loginView: LOGIN_VIEW.LOADING});
|
||||
this.trySsoLogin();
|
||||
|
@ -105,18 +105,6 @@ export default class SoftLogout extends React.Component {
|
|||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
|
||||
if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const appUrl = url.parse(window.location.href, true);
|
||||
appUrl.hash = ""; // Clear #/soft_logout off the URL
|
||||
appUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
appUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
|
||||
const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso");
|
||||
this.setState({ssoUrl});
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
|
@ -170,8 +158,8 @@ export default class SoftLogout extends React.Component {
|
|||
async trySsoLogin() {
|
||||
this.setState({busy: true});
|
||||
|
||||
const hsUrl = this.props.realQueryParams['homeserver'];
|
||||
const isUrl = this.props.realQueryParams['identityServer'] || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const hsUrl = localStorage.getItem(SSO_HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(SSO_ID_SERVER_URL_KEY) || MatrixClientPeg.get().getIdentityServerUrl();
|
||||
const loginType = "m.login.token";
|
||||
const loginParams = {
|
||||
token: this.props.realQueryParams['loginToken'],
|
||||
|
@ -195,14 +183,6 @@ export default class SoftLogout extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onSsoLogin = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
window.location.href = this.state.ssoUrl;
|
||||
};
|
||||
|
||||
_renderSignInSection() {
|
||||
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
@ -234,7 +214,6 @@ export default class SoftLogout extends React.Component {
|
|||
<p>{introText}</p>
|
||||
{error}
|
||||
<Field
|
||||
id="softlogout_password"
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
onChange={this.onPasswordChange}
|
||||
|
@ -257,8 +236,6 @@ export default class SoftLogout extends React.Component {
|
|||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
if (!introText) {
|
||||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
|
@ -266,9 +243,11 @@ export default class SoftLogout extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<AccessibleButton kind='primary' onClick={this.onSsoLogin}>
|
||||
{_t('Sign in with single sign-on')}
|
||||
</AccessibleButton>
|
||||
<SSOButton
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,12 +16,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'AuthHeader',
|
||||
|
||||
propTypes: {
|
||||
disableLanguageSelector: PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
|
||||
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
|
||||
|
@ -29,7 +34,7 @@ export default createReactClass({
|
|||
return (
|
||||
<div className="mx_AuthHeader">
|
||||
<AuthHeaderLogo />
|
||||
<LanguageSelector />
|
||||
<LanguageSelector disabled={this.props.disableLanguageSelector} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -46,7 +46,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
|
|
|
@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
import { COUNTRIES } from '../../../phonenumber';
|
||||
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default class CountryDropdown extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (!this.props.value) {
|
||||
// If no value is given, we start with the default
|
||||
// country selected, but our parent component
|
||||
|
@ -80,7 +80,7 @@ export default class CountryDropdown extends React.Component {
|
|||
}
|
||||
|
||||
_flagImgForIso2(iso2) {
|
||||
return <img src={require(`../../../../res/img/flags/${iso2}.png`)} />;
|
||||
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
|
||||
}
|
||||
|
||||
_getShortOption(iso2) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,11 +18,13 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CustomServerDialog',
|
||||
|
||||
render: function() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_Dialog_title">
|
||||
|
@ -32,8 +34,9 @@ export default createReactClass({
|
|||
<p>{_t(
|
||||
"You can use the custom server options to sign into other " +
|
||||
"Matrix servers by specifying a different homeserver URL. This " +
|
||||
"allows you to use this app with an existing Matrix account on a " +
|
||||
"allows you to use %(brand)s with an existing Matrix account on a " +
|
||||
"different homeserver.",
|
||||
{ brand },
|
||||
)}</p>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,6 +25,7 @@ import classnames from 'classnames';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -59,11 +60,21 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
* session to be failed and the process to go back to the start.
|
||||
* setEmailSid: m.login.email.identity only: a function to be called with the
|
||||
* email sid after a token is requested.
|
||||
* onPhaseChange: A function which is called when the stage's phase changes. If
|
||||
* the stage has no phases, call this with DEFAULT_PHASE. Takes
|
||||
* one argument, the phase, and is always defined/required.
|
||||
* continueText: For stages which have a continue button, the text to use.
|
||||
* continueKind: For stages which have a continue button, the style of button to
|
||||
* use. For example, 'danger' or 'primary'.
|
||||
* onCancel A function with no arguments which is called by the stage if the
|
||||
* user knowingly cancelled/dismissed the authentication attempt.
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
export const DEFAULT_PHASE = 0;
|
||||
|
||||
export const PasswordAuthEntry = createReactClass({
|
||||
displayName: 'PasswordAuthEntry',
|
||||
|
||||
|
@ -78,6 +89,11 @@ export const PasswordAuthEntry = createReactClass({
|
|||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -145,7 +161,6 @@ export const PasswordAuthEntry = createReactClass({
|
|||
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
|
||||
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
|
||||
<Field
|
||||
id="mx_InteractiveAuthEntryComponents_password"
|
||||
className={passwordBoxClass}
|
||||
type="password"
|
||||
name="passwordField"
|
||||
|
@ -176,6 +191,11 @@ export const RecaptchaAuthEntry = createReactClass({
|
|||
stageParams: PropTypes.object.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
|
@ -237,8 +257,14 @@ export const TermsAuthEntry = createReactClass({
|
|||
errorText: PropTypes.string,
|
||||
busy: PropTypes.bool,
|
||||
showContinue: PropTypes.bool,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
componentWillMount: function() {
|
||||
// example stageParams:
|
||||
//
|
||||
|
@ -329,6 +355,7 @@ export const TermsAuthEntry = createReactClass({
|
|||
allChecked = allChecked && checked;
|
||||
|
||||
checkboxes.push(
|
||||
// XXX: replace with StyledCheckbox
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
|
@ -379,16 +406,21 @@ export const EmailIdentityAuthEntry = createReactClass({
|
|||
stageState: PropTypes.object.isRequired,
|
||||
fail: PropTypes.func.isRequired,
|
||||
setEmailSid: PropTypes.func.isRequired,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
requestingToken: false,
|
||||
};
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.requestingToken) {
|
||||
// This component is now only displayed once the token has been requested,
|
||||
// so we know the email has been sent. It can also get loaded after the user
|
||||
// has clicked the validation link if the server takes a while to propagate
|
||||
// the validation internally. If we're in the session spawned from clicking
|
||||
// the validation link, we won't know the email address, so if we don't have it,
|
||||
// assume that the link has been clicked and the server will realise when we poll.
|
||||
if (this.props.inputs.emailAddress === undefined) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return <Loader />;
|
||||
} else {
|
||||
|
@ -421,6 +453,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
clientSecret: PropTypes.func,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -430,7 +463,9 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
|
||||
this._submitUrl = null;
|
||||
this._sid = null;
|
||||
this._msisdn = null;
|
||||
|
@ -504,6 +539,7 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
});
|
||||
|
@ -565,6 +601,91 @@ export const MsisdnAuthEntry = createReactClass({
|
|||
},
|
||||
});
|
||||
|
||||
export class SSOAuthEntry extends React.Component {
|
||||
static propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
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";
|
||||
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso";
|
||||
|
||||
static PHASE_PREAUTH = 1; // button to start SSO
|
||||
static PHASE_POSTAUTH = 2; // button to confirm SSO completed
|
||||
|
||||
_ssoUrl: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// We actually send the user through fallback auth so we don't have to
|
||||
// deal with a redirect back to us, losing application context.
|
||||
this._ssoUrl = props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
);
|
||||
|
||||
this.state = {
|
||||
phase: SSOAuthEntry.PHASE_PREAUTH,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
|
||||
}
|
||||
|
||||
onStartAuthClick = () => {
|
||||
// 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
|
||||
// context.
|
||||
|
||||
window.open(this._ssoUrl, '_blank');
|
||||
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
|
||||
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
|
||||
};
|
||||
|
||||
onConfirmClick = () => {
|
||||
this.props.submitAuthDict({});
|
||||
};
|
||||
|
||||
render() {
|
||||
let continueButton = null;
|
||||
const cancelButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.props.onCancel}
|
||||
kind={this.props.continueKind ? (this.props.continueKind + '_outline') : 'primary_outline'}
|
||||
>{_t("Cancel")}</AccessibleButton>
|
||||
);
|
||||
if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onStartAuthClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Single Sign On")}</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
continueButton = (
|
||||
<AccessibleButton
|
||||
onClick={this.onConfirmClick}
|
||||
kind={this.props.continueKind || 'primary'}
|
||||
>{this.props.continueText || _t("Confirm")}</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className='mx_InteractiveAuthEntryComponents_sso_buttons'>
|
||||
{cancelButton}
|
||||
{continueButton}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export const FallbackAuthEntry = createReactClass({
|
||||
displayName: 'FallbackAuthEntry',
|
||||
|
||||
|
@ -574,9 +695,15 @@ export const FallbackAuthEntry = createReactClass({
|
|||
loginType: PropTypes.string.isRequired,
|
||||
submitAuthDict: PropTypes.func.isRequired,
|
||||
errorText: PropTypes.string,
|
||||
onPhaseChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this.props.onPhaseChange(DEFAULT_PHASE);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// we have to make the user click a button, as browsers will block
|
||||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
|
@ -598,7 +725,10 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onShowFallbackClick: function() {
|
||||
_onShowFallbackClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const url = this.props.matrixClient.getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId,
|
||||
|
@ -627,7 +757,7 @@ export const FallbackAuthEntry = createReactClass({
|
|||
}
|
||||
return (
|
||||
<div>
|
||||
<a ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
|
@ -640,11 +770,12 @@ const AuthEntryComponents = [
|
|||
EmailIdentityAuthEntry,
|
||||
MsisdnAuthEntry,
|
||||
TermsAuthEntry,
|
||||
SSOAuthEntry,
|
||||
];
|
||||
|
||||
export default function getEntryComponentForLoginType(loginType) {
|
||||
for (const c of AuthEntryComponents) {
|
||||
if (c.LOGIN_TYPE == loginType) {
|
||||
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,12 +28,14 @@ function onChange(newLang) {
|
|||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector() {
|
||||
export default function LanguageSelector({disabled}) {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <LanguageDropdown className="mx_AuthBody_language"
|
||||
return <LanguageDropdown
|
||||
className="mx_AuthBody_language"
|
||||
onOptionChange={onChange}
|
||||
value={getCurrentLanguage()}
|
||||
disabled={disabled}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
|||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
import ServerConfig from "./ServerConfig";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
// TODO: TravisR - Can this extend ServerConfig for most things?
|
||||
|
||||
|
@ -94,10 +95,10 @@ export default class ModularServerConfig extends ServerConfig {
|
|||
|
||||
return (
|
||||
<div className="mx_ServerConfig">
|
||||
<h3>{_t("Your Modular server")}</h3>
|
||||
<h3>{_t("Your server")}</h3>
|
||||
{_t(
|
||||
"Enter the location of your Modular homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
||||
"Enter the location of your Element Matrix Services homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>element.io</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
|
@ -106,7 +107,8 @@ export default class ModularServerConfig extends ServerConfig {
|
|||
)}
|
||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
||||
<div className="mx_ServerConfig_fields">
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Server Name")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
|
|
125
src/components/views/auth/PassphraseField.tsx
Normal file
125
src/components/views/auth/PassphraseField.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {PureComponent, RefCallback, RefObject} from "react";
|
||||
import classNames from "classnames";
|
||||
import zxcvbn from "zxcvbn";
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import Field from "../elements/Field";
|
||||
|
||||
interface IProps {
|
||||
autoFocus?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
minScore: 0 | 1 | 2 | 3 | 4;
|
||||
value: string;
|
||||
fieldRef?: RefCallback<Field> | RefObject<Field>;
|
||||
|
||||
label?: string;
|
||||
labelEnterPassword?: string;
|
||||
labelStrongPassword?: string;
|
||||
labelAllowedButUnsafe?: string;
|
||||
|
||||
onChange(ev: React.FormEvent<HTMLElement>);
|
||||
onValidate(result: IValidationResult);
|
||||
}
|
||||
|
||||
interface IState {
|
||||
complexity: zxcvbn.ZXCVBNResult;
|
||||
}
|
||||
|
||||
class PassphraseField extends PureComponent<IProps, IState> {
|
||||
static defaultProps = {
|
||||
label: _td("Password"),
|
||||
labelEnterPassword: _td("Enter password"),
|
||||
labelStrongPassword: _td("Nice, strong password!"),
|
||||
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
|
||||
};
|
||||
|
||||
state = { complexity: null };
|
||||
|
||||
public readonly validate = withValidation<this>({
|
||||
description: function() {
|
||||
const complexity = this.state.complexity;
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t(this.props.labelEnterPassword),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({ complexity });
|
||||
const safe = complexity.score >= this.props.minScore;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (this.state.complexity.score >= this.props.minScore) {
|
||||
return _t(this.props.labelStrongPassword);
|
||||
}
|
||||
return _t(this.props.labelAllowedButUnsafe);
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.complexity;
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onValidate = async (fieldState: IFieldState) => {
|
||||
const result = await this.validate(fieldState);
|
||||
this.props.onValidate(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Field
|
||||
id={this.props.id}
|
||||
autoFocus={this.props.autoFocus}
|
||||
className={classNames("mx_PassphraseField", this.props.className)}
|
||||
ref={this.props.fieldRef}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t(this.props.label)}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
onValidate={this.onValidate}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export default PassphraseField;
|
|
@ -23,6 +23,7 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a username/password form.
|
||||
|
@ -44,6 +45,7 @@ export default class PasswordLogin extends React.Component {
|
|||
loginIncorrect: PropTypes.bool,
|
||||
disableSubmit: PropTypes.bool,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
busy: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -183,7 +185,7 @@ export default class PasswordLogin extends React.Component {
|
|||
this.props.onPasswordChanged(ev.target.value);
|
||||
}
|
||||
|
||||
renderLoginField(loginType) {
|
||||
renderLoginField(loginType, autoFocus) {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
const classes = {};
|
||||
|
@ -193,7 +195,6 @@ export default class PasswordLogin extends React.Component {
|
|||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_email"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="email_input"
|
||||
type="text"
|
||||
|
@ -203,13 +204,12 @@ export default class PasswordLogin extends React.Component {
|
|||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_username"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
key="username_input"
|
||||
type="text"
|
||||
|
@ -218,7 +218,7 @@ export default class PasswordLogin extends React.Component {
|
|||
onChange={this.onUsernameChanged}
|
||||
onBlur={this.onUsernameBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
|
@ -233,17 +233,16 @@ export default class PasswordLogin extends React.Component {
|
|||
|
||||
return <Field
|
||||
className={classNames(classes)}
|
||||
id="mx_PasswordLogin_phoneNumber"
|
||||
name="phoneNumber"
|
||||
key="phone_input"
|
||||
type="text"
|
||||
label={_t("Phone")}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChanged}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus
|
||||
autoFocus={autoFocus}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
@ -268,12 +267,16 @@ export default class PasswordLogin extends React.Component {
|
|||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = <span>
|
||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||
a: sub => <a className="mx_Login_forgot"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
href="#"
|
||||
>
|
||||
{sub}
|
||||
</a>,
|
||||
a: sub => (
|
||||
<AccessibleButton
|
||||
className="mx_Login_forgot"
|
||||
disabled={this.props.busy}
|
||||
kind="link"
|
||||
onClick={this.onForgotPasswordClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
})}
|
||||
</span>;
|
||||
}
|
||||
|
@ -282,7 +285,10 @@ export default class PasswordLogin extends React.Component {
|
|||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const loginField = this.renderLoginField(this.state.loginType);
|
||||
// If login is empty, autoFocus login, otherwise autoFocus password.
|
||||
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
||||
const autoFocusPassword = !this.isLoginEmpty();
|
||||
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
||||
|
||||
let loginType;
|
||||
if (!SdkConfig.get().disable_3pid_login) {
|
||||
|
@ -290,7 +296,6 @@ export default class PasswordLogin extends React.Component {
|
|||
<div className="mx_Login_type_container">
|
||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||
<Field
|
||||
id="mx_PasswordLogin_type"
|
||||
element="select"
|
||||
value={this.state.loginType}
|
||||
onChange={this.onLoginTypeChange}
|
||||
|
@ -328,20 +333,20 @@ export default class PasswordLogin extends React.Component {
|
|||
{loginField}
|
||||
<Field
|
||||
className={pwFieldClass}
|
||||
id="mx_PasswordLogin_password"
|
||||
type="password"
|
||||
name="password"
|
||||
label={_t('Password')}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChanged}
|
||||
disabled={this.props.disableSubmit}
|
||||
autoFocus={autoFocusPassword}
|
||||
/>
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit"
|
||||
{ !this.props.busy && <input className="mx_Login_submit"
|
||||
type="submit"
|
||||
value={_t('Sign in')}
|
||||
disabled={this.props.disableSubmit}
|
||||
/>
|
||||
/> }
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import PassphraseField from "./PassphraseField";
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -76,9 +77,8 @@ export default createReactClass({
|
|||
email: this.props.defaultEmail || "",
|
||||
phoneNumber: this.props.defaultPhoneNumber || "",
|
||||
password: this.props.defaultPassword || "",
|
||||
passwordConfirm: "",
|
||||
passwordConfirm: this.props.defaultPassword || "",
|
||||
passwordComplexity: null,
|
||||
passwordSafe: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -102,11 +102,15 @@ export default createReactClass({
|
|||
"No identity server is configured so you cannot add an email address in order to " +
|
||||
"reset your password in the future.",
|
||||
);
|
||||
} else {
|
||||
} else if (this._showEmail()) {
|
||||
desc = _t(
|
||||
"If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?",
|
||||
);
|
||||
} else {
|
||||
// user can't set an e-mail so don't prompt them to
|
||||
self._doSubmit(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
@ -260,65 +264,10 @@ export default createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
async onPasswordValidate(fieldState) {
|
||||
const result = await this.validatePasswordRules(fieldState);
|
||||
onPasswordValidate(result) {
|
||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validatePasswordRules: withValidation({
|
||||
description: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress
|
||||
className="mx_AuthBody_passwordScore"
|
||||
max={PASSWORD_MIN_SCORE}
|
||||
value={score}
|
||||
/>;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Enter password"),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
const safe = complexity.score >= PASSWORD_MIN_SCORE;
|
||||
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
|
||||
this.setState({
|
||||
passwordComplexity: complexity,
|
||||
passwordSafe: safe,
|
||||
});
|
||||
return allowUnsafe || safe;
|
||||
},
|
||||
valid: function() {
|
||||
// Unsafe passwords that are valid are only possible through a
|
||||
// configuration flag. We'll print some helper text to signal
|
||||
// to the user that their password is allowed, but unsafe.
|
||||
if (!this.state.passwordSafe) {
|
||||
return _t("Password is allowed, but unsafe");
|
||||
}
|
||||
return _t("Nice, strong password!");
|
||||
},
|
||||
invalid: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onPasswordConfirmChange(ev) {
|
||||
this.setState({
|
||||
passwordConfirm: ev.target.value,
|
||||
|
@ -470,7 +419,6 @@ export default createReactClass({
|
|||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
id="mx_RegistrationForm_email"
|
||||
ref={field => this[FIELD_EMAIL] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
|
@ -481,13 +429,10 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
renderPassword() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
return <PassphraseField
|
||||
id="mx_RegistrationForm_password"
|
||||
ref={field => this[FIELD_PASSWORD] = field}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={_t("Password")}
|
||||
fieldRef={field => this[FIELD_PASSWORD] = field}
|
||||
minScore={PASSWORD_MIN_SCORE}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
onValidate={this.onPasswordValidate}
|
||||
|
@ -524,12 +469,11 @@ export default createReactClass({
|
|||
onOptionChange={this.onPhoneCountryChange}
|
||||
/>;
|
||||
return <Field
|
||||
id="mx_RegistrationForm_phoneNumber"
|
||||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
prefixComponent={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
/>;
|
||||
|
|
|
@ -72,7 +72,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
if (newProps.serverConfig.hsUrl === this.state.hsUrl &&
|
||||
newProps.serverConfig.isUrl === this.state.isUrl) return;
|
||||
|
||||
|
@ -223,7 +224,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_hsUrl"
|
||||
<Field
|
||||
id="mx_ServerConfig_hsUrl"
|
||||
label={_t("Homeserver URL")}
|
||||
placeholder={this.props.serverConfig.hsUrl}
|
||||
value={this.state.hsUrl}
|
||||
|
@ -246,7 +248,7 @@ export default class ServerConfig extends React.PureComponent {
|
|||
{sub}
|
||||
</a>,
|
||||
})}
|
||||
<Field id="mx_ServerConfig_isUrl"
|
||||
<Field
|
||||
label={_t("Identity Server URL")}
|
||||
placeholder={this.props.serverConfig.isUrl}
|
||||
value={this.state.isUrl || ''}
|
||||
|
|
|
@ -22,7 +22,8 @@ import classnames from 'classnames';
|
|||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import {makeType} from "../../../utils/TypeUtils";
|
||||
|
||||
const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication';
|
||||
const MODULAR_URL = 'https://element.io/matrix-services' +
|
||||
'?utm_source=element-web&utm_medium=web&utm_campaign=element-web-authentication';
|
||||
|
||||
export const FREE = 'Free';
|
||||
export const PREMIUM = 'Premium';
|
||||
|
@ -44,7 +45,7 @@ export const TYPES = {
|
|||
PREMIUM: {
|
||||
id: PREMIUM,
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||
logo: () => <img src={require('../../../../res/img/ems-logo.svg')} height={16} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
|
|
|
@ -18,6 +18,10 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import AuthPage from "./AuthPage";
|
||||
import {_td} from "../../../languageHandler";
|
||||
|
||||
// translatable strings for Welcome pages
|
||||
_td("Sign in with SSO");
|
||||
|
||||
export default class Welcome extends React.PureComponent {
|
||||
render() {
|
||||
|
@ -36,8 +40,13 @@ export default class Welcome extends React.PureComponent {
|
|||
return (
|
||||
<AuthPage>
|
||||
<div className="mx_Welcome">
|
||||
<EmbeddedPage className="mx_WelcomePage"
|
||||
<EmbeddedPage
|
||||
className="mx_WelcomePage"
|
||||
url={pageUrl}
|
||||
replaceMap={{
|
||||
"$riot:ssoUrl": "#/start_sso",
|
||||
"$riot:casUrl": "#/start_cas",
|
||||
}}
|
||||
/>
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
|
|
|
@ -1,222 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
let urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
urls = Array.from(new Set(urls));
|
||||
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{ fontSize: (width * 0.65) + "px",
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
width={width} height={height} aria-hidden="true" />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
width={width} height={height}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
199
src/components/views/avatars/BaseAvatar.tsx
Normal file
199
src/components/views/avatars/BaseAvatar.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
interface IProps {
|
||||
name: string; // The name (first initial used as default)
|
||||
idName?: string; // ID for generating hash colours
|
||||
title?: string; // onHover title text
|
||||
url?: string; // highest priority of them all, shortcut to set in urls[0]
|
||||
urls?: string[]; // [highest_priority, ... , lowest_priority]
|
||||
width?: number;
|
||||
height?: number;
|
||||
// XXX: resizeMethod not actually used.
|
||||
resizeMethod?: string;
|
||||
defaultToInitialLetter?: boolean; // true to add default url
|
||||
onClick?: React.MouseEventHandler;
|
||||
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||
const [imageUrls, setUrls] = useState<string[]>([]);
|
||||
const [urlsIndex, setIndex] = useState<number>();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
setIndex(i => i + 1); // try the next one
|
||||
}, []);
|
||||
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
_urls = memoizedUrls || [];
|
||||
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
}, []);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const BaseAvatar = (props: IProps) => {
|
||||
const {
|
||||
name,
|
||||
idName,
|
||||
title,
|
||||
url,
|
||||
urls,
|
||||
width = 40,
|
||||
height = 40,
|
||||
resizeMethod = "crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter = true,
|
||||
onClick,
|
||||
inputRef,
|
||||
className,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, onError] = useImageUrl({url, urls});
|
||||
|
||||
if (!imageUrl && defaultToInitialLetter) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (onClick !== null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
onClick={onClick}
|
||||
inputRef={inputRef}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span
|
||||
className={classNames("mx_BaseAvatar", className)}
|
||||
ref={inputRef}
|
||||
{...otherProps}
|
||||
role="presentation"
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick !== null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
export type BaseAvatarType = React.FC<IProps>;
|
73
src/components/views/avatars/DecoratedRoomAvatar.tsx
Normal file
73
src/components/views/avatars/DecoratedRoomAvatar.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { TagID } from '../../../stores/room-list/models';
|
||||
import RoomAvatar from "./RoomAvatar";
|
||||
import RoomTileIcon from "../rooms/RoomTileIcon";
|
||||
import NotificationBadge from '../rooms/NotificationBadge';
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
avatarSize: number;
|
||||
tag: TagID;
|
||||
displayBadge?: boolean;
|
||||
forceCount?: boolean;
|
||||
oobData?: object;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
notificationState?: NotificationState;
|
||||
}
|
||||
|
||||
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
let badge: React.ReactNode;
|
||||
if (this.props.displayBadge) {
|
||||
badge = <NotificationBadge
|
||||
notification={this.state.notificationState}
|
||||
forceCount={this.props.forceCount}
|
||||
roomId={this.props.room.roomId}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_DecoratedRoomAvatar">
|
||||
<RoomAvatar
|
||||
room={this.props.room}
|
||||
width={this.props.avatarSize}
|
||||
height={this.props.avatarSize}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={this.props.viewAvatarOnClick}
|
||||
/>
|
||||
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
|
||||
{badge}
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -15,43 +15,36 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'GroupAvatar',
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
groupName?: string;
|
||||
groupAvatarUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
export default class GroupAvatar extends React.Component<IProps> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getGroupAvatarUrl: function() {
|
||||
getGroupAvatarUrl() {
|
||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||
this.props.groupAvatarUrl,
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
render() {
|
||||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
|
@ -65,5 +58,5 @@ export default createReactClass({
|
|||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -16,54 +16,62 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import BaseAvatar from "./BaseAvatar";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberAvatar',
|
||||
interface IProps {
|
||||
// TODO: replace with correct type
|
||||
member: any;
|
||||
fallbackUserId: string;
|
||||
width: number;
|
||||
height: number;
|
||||
resizeMethod: string;
|
||||
// The onClick to give the avatar
|
||||
onClick: React.MouseEventHandler;
|
||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||
viewUserOnClick: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
propTypes: {
|
||||
member: PropTypes.object,
|
||||
fallbackUserId: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
interface IState {
|
||||
name: string;
|
||||
title: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
viewUserOnClick: false,
|
||||
};
|
||||
},
|
||||
export default class MemberAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
viewUserOnClick: false,
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState(this._getState(nextProps));
|
||||
},
|
||||
this.state = MemberAvatar.getState(props);
|
||||
}
|
||||
|
||||
_getState: function(props) {
|
||||
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||
return MemberAvatar.getState(nextProps);
|
||||
}
|
||||
|
||||
private static getState(props: IProps): IState {
|
||||
if (props.member && props.member.name) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod),
|
||||
imageUrl: props.member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
};
|
||||
} else if (props.fallbackUserId) {
|
||||
return {
|
||||
|
@ -73,18 +81,16 @@ export default createReactClass({
|
|||
} else {
|
||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
}
|
||||
|
||||
render() {
|
||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
const userId = member ? member.userId : fallbackUserId;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
action: Action.ViewUser,
|
||||
member: this.props.member,
|
||||
});
|
||||
};
|
||||
|
@ -94,5 +100,5 @@ export default createReactClass({
|
|||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
|
|||
this._button = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
|
||||
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,23 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const GroupsButton = function(props) {
|
||||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton className="mx_GroupsButton" action="toggle_my_groups"
|
||||
label={_t("Communities")}
|
||||
size={props.size}
|
||||
tooltip={true}
|
||||
/>
|
||||
);
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
const PulsedAvatar: React.FC<IProps> = (props) => {
|
||||
return <div className="mx_PulsedAvatar">
|
||||
{props.children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
GroupsButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
};
|
||||
|
||||
export default GroupsButton;
|
||||
export default PulsedAvatar;
|
|
@ -13,89 +13,96 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import React from 'react';
|
||||
import Room from 'matrix-js-sdk/src/models/room';
|
||||
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import ImageView from '../elements/ImageView';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from "../../../index";
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
||||
interface IProps {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
propTypes: {
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
viewAvatarOnClick: PropTypes.bool,
|
||||
},
|
||||
room?: Room;
|
||||
// TODO: type when js-sdk has types
|
||||
oobData?: any;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: string;
|
||||
viewAvatarOnClick?: boolean;
|
||||
}
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
oobData: {},
|
||||
interface IState {
|
||||
urls: string[];
|
||||
}
|
||||
|
||||
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
oobData: {},
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
urls: RoomAvatar.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
public componentDidMount() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||
return {
|
||||
urls: RoomAvatar.getImageUrls(nextProps),
|
||||
};
|
||||
}
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
// TODO: type when js-sdk has types
|
||||
private onRoomStateEvents = (ev: any) => {
|
||||
if (!this.props.room ||
|
||||
ev.getRoomId() !== this.props.room.roomId ||
|
||||
ev.getType() !== 'm.room.avatar'
|
||||
) return;
|
||||
|
||||
this.setState({
|
||||
urls: this.getImageUrls(this.props),
|
||||
urls: RoomAvatar.getImageUrls(this.props),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
getImageUrls: function(props) {
|
||||
private static getImageUrls(props: IProps): string[] {
|
||||
return [
|
||||
getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
// Default props don't play nicely with getDerivedStateFromProps
|
||||
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
||||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
RoomAvatar.getRoomAvatarUrl(props),
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
return (url !== null && url !== "");
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getRoomAvatarUrl: function(props) {
|
||||
private static getRoomAvatarUrl(props: IProps): string {
|
||||
if (!props.room) return null;
|
||||
|
||||
return Avatar.avatarUrlForRoom(
|
||||
|
@ -104,35 +111,32 @@ export default createReactClass({
|
|||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onRoomAvatarClick: function() {
|
||||
private onRoomAvatarClick = () => {
|
||||
const avatarUrl = this.props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
null, null, null, false);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: avatarUrl,
|
||||
name: this.props.room.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
public render() {
|
||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
<BaseAvatar {...otherProps}
|
||||
name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls}
|
||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null}
|
||||
disabled={!this.state.urls[0]} />
|
||||
onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ import createReactClass from 'create-react-class';
|
|||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -61,7 +61,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions);
|
||||
this._checkPermissions();
|
||||
},
|
||||
|
@ -116,11 +116,6 @@ export default createReactClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
e2eInfoClicked: function() {
|
||||
this.props.e2eInfoCallback();
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReportEventClick: function() {
|
||||
const ReportEventDialog = sdk.getComponent("dialogs.ReportEventDialog");
|
||||
Modal.createTrackedDialog('Report Event', '', ReportEventDialog, {
|
||||
|
@ -130,22 +125,24 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
onViewSourceClick: function() {
|
||||
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
content: this.props.mxEvent.event,
|
||||
roomId: ev.getRoomId(),
|
||||
eventId: ev.getId(),
|
||||
content: ev.event,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onViewClearSourceClick: function() {
|
||||
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
|
||||
const ViewSource = sdk.getComponent('structures.ViewSource');
|
||||
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
|
||||
roomId: this.props.mxEvent.getRoomId(),
|
||||
eventId: this.props.mxEvent.getId(),
|
||||
roomId: ev.getRoomId(),
|
||||
eventId: ev.getId(),
|
||||
// FIXME: _clearEvent is private
|
||||
content: this.props.mxEvent._clearEvent,
|
||||
content: ev._clearEvent,
|
||||
}, 'mx_Dialog_viewsource');
|
||||
this.closeMenu();
|
||||
},
|
||||
|
@ -463,15 +460,6 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let e2eInfo;
|
||||
if (this.props.e2eInfoCallback) {
|
||||
e2eInfo = (
|
||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.e2eInfoClicked}>
|
||||
{ _t('End-to-end encryption information') }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let reportEventButton;
|
||||
if (mxEvent.getSender() !== me) {
|
||||
reportEventButton = (
|
||||
|
@ -498,7 +486,6 @@ export default createReactClass({
|
|||
{ quoteButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
{ reportEventButton }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ import classNames from 'classnames';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import * as Rooms from '../../../Rooms';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
|
@ -82,7 +82,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
},
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
if (!user) {
|
||||
return;
|
||||
|
|
|
@ -18,9 +18,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import * as sdk from '../../../index';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
|
@ -54,21 +53,12 @@ export default class TagTileContextMenu extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick}>
|
||||
<TintableSvg
|
||||
className="mx_TagTileContextMenu_item_icon"
|
||||
src={require("../../../../res/img/icons-groups.svg")}
|
||||
width="15"
|
||||
height="15"
|
||||
/>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
</MenuItem>
|
||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick}>
|
||||
<img className="mx_TagTileContextMenu_item_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t('Hide') }
|
||||
</MenuItem>
|
||||
</div>;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import LogoutDialog from "../dialogs/LogoutDialog";
|
||||
import Modal from "../../../Modal";
|
||||
|
@ -26,6 +26,8 @@ import { getHostingLink } from '../../../utils/HostingLink';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
import * as sdk from "../../../index";
|
||||
import {getHomePageUrl} from "../../../utils/pages";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
export default class TopLeftMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -47,15 +49,7 @@ export default class TopLeftMenu extends React.Component {
|
|||
}
|
||||
|
||||
hasHomePage() {
|
||||
const config = SdkConfig.get();
|
||||
const pagesConfig = config.embeddedPages;
|
||||
if (pagesConfig && pagesConfig.homeUrl) {
|
||||
return true;
|
||||
}
|
||||
// This is a deprecated config option for the home page
|
||||
// (despite the name, given we also now have a welcome
|
||||
// page, which is not the same).
|
||||
return !!config.welcomePageUrl;
|
||||
return !!getHomePageUrl(SdkConfig.get());
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -141,7 +135,7 @@ export default class TopLeftMenu extends React.Component {
|
|||
}
|
||||
|
||||
openSettings() {
|
||||
dis.dispatch({action: 'view_user_settings'});
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ export default createReactClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias} />
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ import createReactClass from 'create-react-class';
|
|||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
|
@ -33,6 +33,7 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../
|
|||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
@ -107,6 +108,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._textinput = createRef();
|
||||
},
|
||||
|
@ -614,7 +616,7 @@ export default createReactClass({
|
|||
|
||||
onManageSettingsClick(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.onCancel();
|
||||
},
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export default createReactClass({
|
|||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus="true">
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -75,8 +75,12 @@ export default createReactClass({
|
|||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element
|
||||
titleClass: PropTypes.string,
|
||||
// optional additional class for the title element (basically anything that can be passed to classnames)
|
||||
titleClass: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -86,7 +90,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
|
@ -143,6 +148,7 @@ export default createReactClass({
|
|||
>
|
||||
<div className={classNames('mx_Dialog_header', {
|
||||
'mx_Dialog_headerWithButton': !!this.props.headerButton,
|
||||
'mx_Dialog_headerWithCancel': !!cancelButton,
|
||||
})}>
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{headerImage}
|
||||
|
|
|
@ -137,12 +137,20 @@ export default class BugReportDialog extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
|
||||
warning = <p><b>
|
||||
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
|
||||
</b></p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
||||
title={_t('Submit debug logs')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ warning }
|
||||
<p>
|
||||
{ _t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
|
@ -166,7 +174,6 @@ export default class BugReportDialog extends React.Component {
|
|||
) }
|
||||
</b></p>
|
||||
<Field
|
||||
id="mx_BugReportDialog_issueUrl"
|
||||
type="text"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
label={_t("GitHub issue")}
|
||||
|
@ -175,7 +182,6 @@ export default class BugReportDialog extends React.Component {
|
|||
placeholder="https://github.com/vector-im/riot-web/issues/..."
|
||||
/>
|
||||
<Field
|
||||
id="mx_BugReportDialog_notes"
|
||||
className="mx_BugReportDialog_field_input"
|
||||
element="textarea"
|
||||
label={_t("Notes")}
|
||||
|
|
|
@ -55,7 +55,8 @@ export default createReactClass({
|
|||
askReason: false,
|
||||
}),
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._reasonField = null;
|
||||
},
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
||||
|
|
|
@ -24,17 +24,20 @@ import withValidation from '../elements/Validation';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
const config = SdkConfig.get();
|
||||
return {
|
||||
isPublic: false,
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: privateShouldBeEncrypted(),
|
||||
name: "",
|
||||
topic: "",
|
||||
alias: "",
|
||||
|
@ -62,6 +65,11 @@ export default createReactClass({
|
|||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = {'m.federate': false};
|
||||
}
|
||||
|
||||
if (!this.state.isPublic) {
|
||||
opts.encryption = this.state.isEncrypted;
|
||||
}
|
||||
|
||||
return opts;
|
||||
},
|
||||
|
||||
|
@ -127,6 +135,10 @@ export default createReactClass({
|
|||
this.setState({isPublic});
|
||||
},
|
||||
|
||||
onEncryptedChange(isEncrypted) {
|
||||
this.setState({isEncrypted});
|
||||
},
|
||||
|
||||
onAliasChange(alias) {
|
||||
this.setState({alias});
|
||||
},
|
||||
|
@ -166,19 +178,38 @@ export default createReactClass({
|
|||
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
|
||||
let privateLabel;
|
||||
let publicLabel;
|
||||
let publicPrivateLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
<RoomAliasField id="alias" ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
|
||||
}
|
||||
|
||||
let e2eeSection;
|
||||
if (!this.state.isPublic) {
|
||||
let microcopy;
|
||||
if (privateShouldBeEncrypted()) {
|
||||
microcopy = _t("You can’t disable this later. Bridges & most bots won’t work yet.");
|
||||
} else {
|
||||
microcopy = _t("Your server admin has disabled end-to-end encryption by default " +
|
||||
"in private rooms & Direct Messages.");
|
||||
}
|
||||
e2eeSection = <React.Fragment>
|
||||
<LabelledToggleSwitch
|
||||
label={ _t("Enable end-to-end encryption")}
|
||||
onChange={this.onEncryptedChange}
|
||||
value={this.state.isEncrypted}
|
||||
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
|
||||
/>
|
||||
<p>{ microcopy }</p>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||
|
@ -188,11 +219,11 @@ export default createReactClass({
|
|||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
<Field id="name" ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field id="topic" label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
|
||||
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
|
||||
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
|
||||
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
|
||||
{ privateLabel }
|
||||
{ publicLabel }
|
||||
{ publicPrivateLabel }
|
||||
{ e2eeSection }
|
||||
{ aliasField }
|
||||
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,11 +17,14 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
export default (props) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
const _onLogoutClicked = () => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Logout e2e db too new', '', QuestionDialog, {
|
||||
|
@ -28,7 +32,8 @@ export default (props) => {
|
|||
description: _t(
|
||||
"To avoid losing your chat history, you must export your room keys " +
|
||||
"before logging out. You will need to go back to the newer version of " +
|
||||
"Riot to do this",
|
||||
"%(brand)s to do this",
|
||||
{ brand },
|
||||
),
|
||||
button: _t("Sign out"),
|
||||
focus: false,
|
||||
|
@ -42,10 +47,11 @@ export default (props) => {
|
|||
};
|
||||
|
||||
const description =
|
||||
_t("You've previously used a newer version of Riot on %(host)s. " +
|
||||
_t(
|
||||
"You've previously used a newer version of %(brand)s with this session. " +
|
||||
"To use this version again with end to end encryption, you will " +
|
||||
"need to sign out and back in again. ",
|
||||
{host: props.host},
|
||||
"need to sign out and back in again.",
|
||||
{ brand },
|
||||
);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,106 +23,161 @@ import Analytics from '../../../Analytics';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
|
||||
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
export default class DeactivateAccountDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
busy: false,
|
||||
shouldErase: false,
|
||||
errStr: null,
|
||||
authData: null, // for UIA
|
||||
authEnabled: true, // see usages for information
|
||||
|
||||
// A few strings that are passed to InteractiveAuth for design or are displayed
|
||||
// next to the InteractiveAuth component.
|
||||
bodyText: null,
|
||||
continueText: null,
|
||||
continueKind: null,
|
||||
};
|
||||
|
||||
this._initAuth(/* shouldErase= */false);
|
||||
}
|
||||
|
||||
_onPasswordFieldChange(ev) {
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
});
|
||||
}
|
||||
_onStagePhaseChange = (stage, phase) => {
|
||||
const dialogAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "danger",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
|
||||
continueText: _t("Confirm account deactivation"),
|
||||
continueKind: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
_onEraseFieldChange(ev) {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
const auth = {
|
||||
type: 'm.login.password',
|
||||
// TODO: Remove `user` once servers support proper UIA
|
||||
// See https://github.com/vector-im/riot-web/issues/10312
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
|
||||
const DEACTIVATE_AESTHETICS = {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
[PasswordAuthEntry.LOGIN_TYPE]: {
|
||||
[DEFAULT_PHASE]: {
|
||||
body: _t("To continue, please enter your password:"),
|
||||
},
|
||||
password: this.state.password,
|
||||
};
|
||||
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||
} catch (err) {
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
||||
errStr = _t('Incorrect password');
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: errStr,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const aesthetics = DEACTIVATE_AESTHETICS[stage];
|
||||
let bodyText = null;
|
||||
let continueText = null;
|
||||
let continueKind = null;
|
||||
if (aesthetics) {
|
||||
const phaseAesthetics = aesthetics[phase];
|
||||
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
|
||||
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
|
||||
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
|
||||
}
|
||||
this.setState({bodyText, continueText, continueKind});
|
||||
};
|
||||
|
||||
_onUIAuthFinished = (success, result, extra) => {
|
||||
if (success) return; // great! makeRequest() will be called too.
|
||||
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this._onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
console.error("Error during UI Auth:", {result, extra});
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
};
|
||||
|
||||
_onUIAuthComplete = (auth) => {
|
||||
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
||||
// Deactivation worked - logout & close this dialog
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(true);
|
||||
}).catch(e => {
|
||||
console.error(e);
|
||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||
});
|
||||
};
|
||||
|
||||
_onEraseFieldChange = (ev) => {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
|
||||
// Disable the auth form because we're going to have to reinitialize the auth
|
||||
// information. We do this because we can't modify the parameters in the UIA
|
||||
// session, and the user will have selected something which changes the request.
|
||||
// Therefore, we throw away the last auth session and try a new one.
|
||||
authEnabled: false,
|
||||
});
|
||||
|
||||
// As mentioned above, set up for auth again to get updated UIA session info
|
||||
this._initAuth(/* shouldErase= */ev.target.checked);
|
||||
};
|
||||
|
||||
_onCancel() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_initAuth(shouldErase) {
|
||||
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
||||
// If we got here, oops. The server didn't require any auth.
|
||||
// Our application lifecycle will catch the error and do the logout bits.
|
||||
// We'll try to log something in an vain attempt to record what happened (storage
|
||||
// is also obliterated on logout).
|
||||
console.warn("User's account got deactivated without confirmation: Server had no auth");
|
||||
this.setState({errStr: _t("Server did not require any authentication")});
|
||||
}).catch(e => {
|
||||
if (e && e.httpStatus === 401 && e.data) {
|
||||
// Valid UIA response
|
||||
this.setState({authData: e.data, authEnabled: true});
|
||||
} else {
|
||||
this.setState({errStr: _t("Server did not return valid authentication information.")});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let passwordBoxClass = '';
|
||||
|
||||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{ this.state.errStr }
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
||||
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
|
||||
const okEnabled = this.state.password && !this.state.busy;
|
||||
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
let auth = <div>{_t("Loading...")}</div>;
|
||||
if (this.state.authData && this.state.authEnabled) {
|
||||
auth = (
|
||||
<div>
|
||||
{this.state.bodyText}
|
||||
<InteractiveAuth
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
authData={this.state.authData}
|
||||
makeRequest={this._onUIAuthComplete}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
onStagePhaseChange={this._onStagePhaseChange}
|
||||
continueText={this.state.continueText}
|
||||
continueKind={this.state.continueKind}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
|
||||
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
|
||||
return (
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}
|
||||
>
|
||||
|
@ -155,46 +210,24 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
|
||||
<div className="mx_DeactivateAccountDialog_input_section">
|
||||
<p>
|
||||
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||
<input
|
||||
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||
type="checkbox"
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
/>
|
||||
{ _t(
|
||||
<StyledCheckbox
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
>
|
||||
{_t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
) }
|
||||
</label>
|
||||
)}
|
||||
</StyledCheckbox>
|
||||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<Field
|
||||
id="mx_DeactivateAccountDialog_password"
|
||||
type="password"
|
||||
label={_t('Password')}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
value={this.state.password}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{error}
|
||||
{auth}
|
||||
</div>
|
||||
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
className="mx_Dialog_primary danger"
|
||||
onClick={this._onOk}
|
||||
disabled={!okEnabled}
|
||||
>
|
||||
{ okLabel }
|
||||
</button>
|
||||
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -1,376 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||
import {ensureDMExists} from "../../../createRoom";
|
||||
import dis from "../../../dispatcher";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
|
||||
|
||||
const MODE_LEGACY = 'legacy';
|
||||
const MODE_SAS = 'sas';
|
||||
|
||||
const PHASE_START = 0;
|
||||
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
|
||||
const PHASE_PICK_VERIFICATION_OPTION = 2;
|
||||
const PHASE_SHOW_SAS = 3;
|
||||
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
|
||||
const PHASE_VERIFIED = 5;
|
||||
const PHASE_CANCELLED = 6;
|
||||
|
||||
export default class DeviceVerifyDialog extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._verifier = null;
|
||||
this._showSasEvent = null;
|
||||
this._request = null;
|
||||
this.state = {
|
||||
phase: PHASE_START,
|
||||
mode: MODE_SAS,
|
||||
sasVerified: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._verifier) {
|
||||
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this._verifier.cancel('User cancel');
|
||||
}
|
||||
}
|
||||
|
||||
_onSwitchToLegacyClick = () => {
|
||||
if (this._verifier) {
|
||||
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this._verifier.cancel('User cancel');
|
||||
this._verifier = null;
|
||||
}
|
||||
this.setState({mode: MODE_LEGACY});
|
||||
}
|
||||
|
||||
_onSwitchToSasClick = () => {
|
||||
this.setState({mode: MODE_SAS});
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onUseSasClick = async () => {
|
||||
try {
|
||||
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
|
||||
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||
// throws upon cancellation
|
||||
await this._verifier.verify();
|
||||
this.setState({phase: PHASE_VERIFIED});
|
||||
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this._verifier = null;
|
||||
} catch (e) {
|
||||
console.log("Verification failed", e);
|
||||
this.setState({
|
||||
phase: PHASE_CANCELLED,
|
||||
});
|
||||
this._verifier = null;
|
||||
this._request = null;
|
||||
}
|
||||
};
|
||||
|
||||
_onLegacyFinished = (confirm) => {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
_onSasRequestClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
|
||||
});
|
||||
const client = MatrixClientPeg.get();
|
||||
const verifyingOwnDevice = this.props.userId === client.getUserId();
|
||||
try {
|
||||
if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
|
||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||
// throws upon cancellation before having started
|
||||
const request = await client.requestVerificationDM(
|
||||
this.props.userId, roomId,
|
||||
);
|
||||
await request.waitFor(r => r.ready || r.started);
|
||||
if (request.ready) {
|
||||
this._verifier = request.beginKeyVerification(verificationMethods.SAS);
|
||||
} else {
|
||||
this._verifier = request.verifier;
|
||||
}
|
||||
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
this._request = await client.requestVerification(this.props.userId, [
|
||||
verificationMethods.SAS,
|
||||
SHOW_QR_CODE_METHOD,
|
||||
verificationMethods.RECIPROCATE_QR_CODE,
|
||||
]);
|
||||
|
||||
await this._request.waitFor(r => r.ready || r.started);
|
||||
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
|
||||
} else {
|
||||
this._verifier = client.beginKeyVerification(
|
||||
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
||||
);
|
||||
}
|
||||
if (!this._verifier) return;
|
||||
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||
// throws upon cancellation
|
||||
await this._verifier.verify();
|
||||
this.setState({phase: PHASE_VERIFIED});
|
||||
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||
this._verifier = null;
|
||||
} catch (e) {
|
||||
console.log("Verification failed", e);
|
||||
this.setState({
|
||||
phase: PHASE_CANCELLED,
|
||||
});
|
||||
this._verifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onSasMatchesClick = () => {
|
||||
this._showSasEvent.confirm();
|
||||
this.setState({
|
||||
phase: PHASE_WAIT_FOR_PARTNER_TO_CONFIRM,
|
||||
});
|
||||
}
|
||||
|
||||
_onVerifiedDoneClick = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onVerifierShowSas = (e) => {
|
||||
this._showSasEvent = e;
|
||||
this.setState({
|
||||
phase: PHASE_SHOW_SAS,
|
||||
});
|
||||
}
|
||||
|
||||
_renderSasVerification() {
|
||||
let body;
|
||||
switch (this.state.phase) {
|
||||
case PHASE_START:
|
||||
body = this._renderVerificationPhaseStart();
|
||||
break;
|
||||
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
|
||||
body = this._renderVerificationPhaseWaitAccept();
|
||||
break;
|
||||
case PHASE_PICK_VERIFICATION_OPTION:
|
||||
body = this._renderVerificationPhasePick();
|
||||
break;
|
||||
case PHASE_SHOW_SAS:
|
||||
body = this._renderSasVerificationPhaseShowSas();
|
||||
break;
|
||||
case PHASE_WAIT_FOR_PARTNER_TO_CONFIRM:
|
||||
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
|
||||
break;
|
||||
case PHASE_VERIFIED:
|
||||
body = this._renderVerificationPhaseVerified();
|
||||
break;
|
||||
case PHASE_CANCELLED:
|
||||
body = this._renderVerificationPhaseCancelled();
|
||||
break;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
|
||||
return (
|
||||
<BaseDialog
|
||||
title={_t("Verify session")}
|
||||
onFinished={this._onCancelClick}
|
||||
>
|
||||
{body}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
_renderVerificationPhaseStart() {
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<div>
|
||||
<AccessibleButton
|
||||
element="span" className="mx_linkButton" onClick={this._onSwitchToLegacyClick}
|
||||
>
|
||||
{_t("Use Legacy Verification (for older clients)")}
|
||||
</AccessibleButton>
|
||||
<p>
|
||||
{ _t("Verify by comparing a short text string.") }
|
||||
</p>
|
||||
<p>
|
||||
{_t("To be secure, do this in person or use a trusted way to communicate.")}
|
||||
</p>
|
||||
<DialogButtons
|
||||
primaryButton={_t('Begin Verifying')}
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={this._onSasRequestClick}
|
||||
onCancel={this._onCancelClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderVerificationPhaseWaitAccept() {
|
||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Spinner />
|
||||
<p>{_t("Waiting for partner to accept...")}</p>
|
||||
<p>{_t(
|
||||
"Nothing appearing? Not all clients support interactive verification yet. " +
|
||||
"<button>Use legacy verification</button>.",
|
||||
{}, {button: sub => <AccessibleButton element='span' className="mx_linkButton"
|
||||
onClick={this._onSwitchToLegacyClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>},
|
||||
)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderVerificationPhasePick() {
|
||||
return <VerificationQREmojiOptions
|
||||
request={this._request}
|
||||
onCancel={this._onCancelClick}
|
||||
onStartEmoji={this._onUseSasClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
_renderSasVerificationPhaseShowSas() {
|
||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||
return <VerificationShowSas
|
||||
sas={this._showSasEvent.sas}
|
||||
onCancel={this._onCancelClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
|
||||
onStartEmoji={this._onUseSasClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
_renderSasVerificationPhaseWaitForPartnerToConfirm() {
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
return <div>
|
||||
<Spinner />
|
||||
<p>{_t(
|
||||
"Waiting for %(userId)s to confirm...", {userId: this.props.userId},
|
||||
)}</p>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_renderVerificationPhaseVerified() {
|
||||
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
|
||||
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
|
||||
}
|
||||
|
||||
_renderVerificationPhaseCancelled() {
|
||||
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
|
||||
return <VerificationCancelled onDone={this._onCancelClick} />;
|
||||
}
|
||||
|
||||
_renderLegacyVerification() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("To verify that this session can be trusted, please check that the key you see " +
|
||||
"in User Settings on that device matches the key below:");
|
||||
} else {
|
||||
text = _t("To verify that this session can be trusted, please contact its owner using some other " +
|
||||
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
|
||||
"for this session matches the key below:");
|
||||
}
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
<AccessibleButton
|
||||
element="span" className="mx_linkButton" onClick={this._onSwitchToSasClick}
|
||||
>
|
||||
{_t("Use two-way text verification")}
|
||||
</AccessibleButton>
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{ _t("If it matches, press the verify button below. " +
|
||||
"If it doesn't, then someone else is intercepting this session " +
|
||||
"and you probably want to press the blacklist button instead.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify session")}
|
||||
description={body}
|
||||
button={_t("I verify that the keys match")}
|
||||
onFinished={this._onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.mode === MODE_LEGACY) {
|
||||
return this._renderLegacyVerification();
|
||||
} else {
|
||||
return <div>
|
||||
{this._renderSasVerification()}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDMExistsAndOpen(userId) {
|
||||
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
|
||||
// we causes us to loose the verifier and restart, and we end up having two verification requests
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
should_peek: false,
|
||||
});
|
||||
return roomId;
|
||||
}
|
|
@ -267,7 +267,8 @@ class FilteredList extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
|
@ -302,7 +303,7 @@ class FilteredList extends React.PureComponent {
|
|||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<Field id="DevtoolsDialog_FilteredList_filter" label={_t('Filter results')} autoFocus={true} size={64}
|
||||
<Field label={_t('Filter results')} autoFocus={true} size={64}
|
||||
type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
|
@ -695,6 +696,9 @@ class VerificationExplorer extends React.Component {
|
|||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.props.onBack}>{_t("Back")}</button>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,7 +196,8 @@ export default class IncomingSasDialog extends React.Component {
|
|||
sas={this._showSasEvent.sas}
|
||||
onCancel={this._onCancelClick}
|
||||
onDone={this._onSasMatchesClick}
|
||||
isSelf={this.props.verifier.userId == MatrixClientPeg.get().getUserId()}
|
||||
isSelf={this.props.verifier.userId === MatrixClientPeg.get().getUserId()}
|
||||
inDialog={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export default createReactClass({
|
|||
button: PropTypes.string,
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -50,10 +51,13 @@ export default createReactClass({
|
|||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_InfoDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
hasCancel={this.props.hasCloseButton}
|
||||
onKeyDown={this.props.onKeyDown}
|
||||
>
|
||||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
|
|
|
@ -18,7 +18,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
export default class IntegrationsDisabledDialog extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -31,7 +32,7 @@ export default class IntegrationsDisabledDialog extends React.Component {
|
|||
|
||||
_onOpenSettingsClick = () => {
|
||||
this.props.onFinished();
|
||||
dis.dispatch({action: "view_user_settings"});
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as sdk from "../../../index";
|
||||
|
||||
export default class IntegrationsImpossibleDialog extends React.Component {
|
||||
|
@ -29,6 +30,7 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
|
@ -39,8 +41,9 @@ export default class IntegrationsImpossibleDialog extends React.Component {
|
|||
<div className='mx_IntegrationsImpossibleDialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Your Riot doesn't allow you to use an Integration Manager to do this. " +
|
||||
"Your %(brand)s doesn't allow you to use an Integration Manager to do this. " +
|
||||
"Please contact an admin.",
|
||||
{ brand },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,6 +24,8 @@ import * as sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
|
||||
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'InteractiveAuthDialog',
|
||||
|
@ -44,12 +47,60 @@ export default createReactClass({
|
|||
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Optional title and body to show when not showing a particular stage
|
||||
title: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
|
||||
// Optional title and body pairs for particular stages and phases within
|
||||
// those stages. Object structure/example is:
|
||||
// {
|
||||
// "org.example.stage_type": {
|
||||
// 1: {
|
||||
// "body": "This is a body for phase 1" of org.example.stage_type,
|
||||
// "title": "Title for phase 1 of org.example.stage_type"
|
||||
// },
|
||||
// 2: {
|
||||
// "body": "This is a body for phase 2 of org.example.stage_type",
|
||||
// "title": "Title for phase 2 of org.example.stage_type"
|
||||
// "continueText": "Confirm identity with Example Auth",
|
||||
// "continueKind": "danger"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Default is defined in _getDefaultDialogAesthetics()
|
||||
aestheticsForStagePhases: PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
|
||||
// See _onUpdateStagePhase()
|
||||
uiaStage: null,
|
||||
uiaStagePhase: null,
|
||||
};
|
||||
},
|
||||
|
||||
_getDefaultDialogAesthetics: function() {
|
||||
const ssoAesthetics = {
|
||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||
title: _t("Use Single Sign On to continue"),
|
||||
body: _t("To continue, use Single Sign On to prove your identity."),
|
||||
continueText: _t("Single Sign On"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
[SSOAuthEntry.PHASE_POSTAUTH]: {
|
||||
title: _t("Confirm to continue"),
|
||||
body: _t("Click the button below to confirm your identity."),
|
||||
continueText: _t("Confirm"),
|
||||
continueKind: "primary",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -57,12 +108,21 @@ export default createReactClass({
|
|||
if (success) {
|
||||
this.props.onFinished(true, result);
|
||||
} else {
|
||||
this.setState({
|
||||
authError: result,
|
||||
});
|
||||
if (result === ERROR_USER_CANCELLED) {
|
||||
this.props.onFinished(false, null);
|
||||
} else {
|
||||
this.setState({
|
||||
authError: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onUpdateStagePhase: function(newStage, newPhase) {
|
||||
// We copy the stage and stage phase params into state for title selection in render()
|
||||
this.setState({uiaStage: newStage, uiaStagePhase: newPhase});
|
||||
},
|
||||
|
||||
_onDismissClick: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
@ -71,6 +131,24 @@ export default createReactClass({
|
|||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
// Let's pick a title, body, and other params text that we'll show to the user. The order
|
||||
// is most specific first, so stagePhase > our props > defaults.
|
||||
|
||||
let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication'));
|
||||
let body = this.state.authError ? null : this.props.body;
|
||||
let continueText = null;
|
||||
let continueKind = null;
|
||||
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
|
||||
if (!this.state.authError && dialogAesthetics) {
|
||||
if (dialogAesthetics[this.state.uiaStage]) {
|
||||
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
|
||||
if (aesthetics && aesthetics.title) title = aesthetics.title;
|
||||
if (aesthetics && aesthetics.body) body = aesthetics.body;
|
||||
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
|
||||
if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind;
|
||||
}
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
|
@ -88,11 +166,16 @@ export default createReactClass({
|
|||
} else {
|
||||
content = (
|
||||
<div id='mx_Dialog_content'>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
{body}
|
||||
<InteractiveAuth
|
||||
ref={this._collectInteractiveAuth}
|
||||
matrixClient={this.props.matrixClient}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onAuthFinished={this._onAuthFinished}
|
||||
onStagePhaseChange={this._onUpdateStagePhase}
|
||||
continueText={continueText}
|
||||
continueKind={continueKind}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -101,7 +184,7 @@ export default createReactClass({
|
|||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
|
|
|
@ -27,14 +27,16 @@ import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
|
@ -125,7 +127,7 @@ class ThreepidMember extends Member {
|
|||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||
onRemove: PropTypes.func, // takes 1 argument, the member being removed
|
||||
};
|
||||
|
||||
_onRemove = (e) => {
|
||||
|
@ -156,18 +158,25 @@ class DMUserTile extends React.PureComponent {
|
|||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
let closeButton;
|
||||
if (this.props.onRemove) {
|
||||
closeButton = (
|
||||
<AccessibleButton
|
||||
className='mx_InviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className='mx_InviteDialog_userTile'>
|
||||
<span className='mx_InviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
{ closeButton }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -219,7 +228,7 @@ class DMRoomTile extends React.PureComponent {
|
|||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < (str.length - 1)) {
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
|
@ -335,10 +344,10 @@ export default class InviteDialog extends React.PureComponent {
|
|||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const taggedRooms = RoomListStoreTempProxy.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
|
@ -564,15 +573,18 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const createRoomOptions = {};
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
// If so, enable encryption in the new room.
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
||||
if (!has3PidMembers) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||
if (allHaveDeviceKeys) {
|
||||
createRoomOptions.encryption = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -640,11 +652,14 @@ export default class InviteDialog extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
// We do not want the user to close the dialog while an action is in progress
|
||||
if (this.state.busy) return;
|
||||
|
||||
this.props.onFinished();
|
||||
_onKeyDown = (e) => {
|
||||
// when the field is empty and the user hits backspace remove the right-most target
|
||||
if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE &&
|
||||
!e.ctrlKey && !e.shiftKey && !e.metaKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
_updateFilter = (e) => {
|
||||
|
@ -888,8 +903,8 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
_onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this._cancel();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
|
@ -906,24 +921,24 @@ export default class InviteDialog extends React.PureComponent {
|
|||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
|
||||
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
||||
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
}
|
||||
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
|
@ -931,7 +946,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
|
@ -943,7 +958,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
|
@ -984,17 +999,18 @@ export default class InviteDialog extends React.PureComponent {
|
|||
|
||||
_renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<textarea
|
||||
key={"input"}
|
||||
rows={1}
|
||||
onKeyDown={this._onKeyDown}
|
||||
onChange={this._updateFilter}
|
||||
value={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
autoFocus={true}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
|
@ -1055,24 +1071,26 @@ export default class InviteDialog extends React.PureComponent {
|
|||
let buttonText;
|
||||
let goButtonFn;
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
if (this.props.kind === KIND_DM) {
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
|
||||
title = _t("Direct Messages");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username, share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
{},
|
||||
{userId: () => {
|
||||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||
}},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
title = _t("Invite to this room");
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||
"<a>share this room</a>.", {},
|
||||
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
|
@ -1087,7 +1105,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
hasCancel={true}
|
||||
onFinished={this._cancel}
|
||||
onFinished={this.props.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className='mx_InviteDialog_content'>
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
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 Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
// TODO: We can remove this once cross-signing is the only way.
|
||||
// https://github.com/vector-im/riot-web/issues/11908
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
* an unverified device.
|
||||
*
|
||||
* onFinished is called with `true` if the key should be shared, `false` if it
|
||||
* should not, and `undefined` if the dialog is cancelled. (In other words:
|
||||
* truthy: do the key share. falsy: don't share the keys).
|
||||
*/
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
deviceInfo: null,
|
||||
wasNewDevice: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
const userId = this.props.userId;
|
||||
const deviceId = this.props.deviceId;
|
||||
|
||||
// give the client a chance to refresh the device list
|
||||
this.props.matrixClient.downloadKeys([userId], false).then((r) => {
|
||||
if (this._unmounted) { return; }
|
||||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for session ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasNewDevice = !deviceInfo.isKnown();
|
||||
|
||||
this.setState({
|
||||
deviceInfo: deviceInfo,
|
||||
wasNewDevice: wasNewDevice,
|
||||
});
|
||||
|
||||
// if the device was new before, it's not any more.
|
||||
if (wasNewDevice) {
|
||||
this.props.matrixClient.setDeviceKnown(
|
||||
userId,
|
||||
deviceId,
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
|
||||
_onVerifyClicked: function() {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
|
||||
console.log("KeyShareDialog: Starting verify dialog");
|
||||
Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, {
|
||||
userId: this.props.userId,
|
||||
device: this.state.deviceInfo,
|
||||
onFinished: (verified) => {
|
||||
if (verified) {
|
||||
// can automatically share the keys now.
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
},
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
},
|
||||
|
||||
_onShareClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'share'");
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onIgnoreClicked: function() {
|
||||
console.log("KeyShareDialog: User clicked 'ignore'");
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_renderContent: function() {
|
||||
const displayName = this.state.deviceInfo.getDisplayName() ||
|
||||
this.state.deviceInfo.deviceId;
|
||||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = _td("You added a new session '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = _td("Your unverified session '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked} autoFocus="true">
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||
|
||||
let content;
|
||||
|
||||
if (this.state.deviceInfo) {
|
||||
content = this._renderContent();
|
||||
} else {
|
||||
content = (
|
||||
<div id='mx_Dialog_content'>
|
||||
<p>{ _t('Loading session info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className='mx_KeyShareRequestDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
110
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
110
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState, useCallback, useRef} from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default function KeySignatureUploadFailedDialog({
|
||||
failures,
|
||||
source,
|
||||
continuation,
|
||||
onFinished,
|
||||
}) {
|
||||
const RETRIES = 2;
|
||||
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const [retry, setRetry] = useState(RETRIES);
|
||||
const [cancelled, setCancelled] = useState(false);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const onCancel = useRef(onFinished);
|
||||
|
||||
const causes = new Map([
|
||||
["_afterCrossSigningLocalKeyChange", _t("a new master key signature")],
|
||||
["checkOwnCrossSigningTrust", _t("a new cross-signing key signature")],
|
||||
["setDeviceVerification", _t("a device cross-signing signature")],
|
||||
]);
|
||||
const defaultCause = _t("a key signature");
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
try {
|
||||
setRetrying(true);
|
||||
const cancel = new Promise((resolve, reject) => {
|
||||
onCancel.current = reject;
|
||||
}).finally(() => {
|
||||
setCancelled(true);
|
||||
});
|
||||
await Promise.race([
|
||||
continuation(),
|
||||
cancel,
|
||||
]);
|
||||
setSuccess(true);
|
||||
} catch (e) {
|
||||
setRetry(r => r-1);
|
||||
} finally {
|
||||
onCancel.current = onFinished;
|
||||
setRetrying(false);
|
||||
}
|
||||
}, [continuation, onFinished]);
|
||||
|
||||
let body;
|
||||
if (!success && !cancelled && continuation && retry > 0) {
|
||||
const reason = causes.get(source) || defaultCause;
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
body = (<div>
|
||||
<p>{_t("%(brand)s encountered an error during upload of:", { brand })}</p>
|
||||
<p>{reason}</p>
|
||||
{retrying && <Spinner />}
|
||||
<pre>{JSON.stringify(failures, null, 2)}</pre>
|
||||
<DialogButtons
|
||||
primaryButton='Retry'
|
||||
hasCancel={true}
|
||||
onPrimaryButtonClick={onRetry}
|
||||
onCancel={onCancel.current}
|
||||
primaryDisabled={retrying}
|
||||
/>
|
||||
</div>);
|
||||
} else {
|
||||
body = (<div>
|
||||
{success ?
|
||||
<span>{_t("Upload completed")}</span> :
|
||||
cancelled ?
|
||||
<span>{_t("Cancelled signature upload")}</span> :
|
||||
<span>{_t("Unable to upload")}</span>}
|
||||
<DialogButtons
|
||||
primaryButton={_t("OK")}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={onFinished}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
title={success ?
|
||||
_t("Signature upload success") :
|
||||
_t("Signature upload failed")}
|
||||
fixedWidth={false}
|
||||
onFinished={() => {}}
|
||||
>
|
||||
{body}
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,17 +18,28 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default (props) => {
|
||||
const description1 =
|
||||
_t("You've previously used Riot on %(host)s with lazy loading of members enabled. " +
|
||||
"In this version lazy loading is disabled. " +
|
||||
"As the local cache is not compatible between these two settings, " +
|
||||
"Riot needs to resync your account.",
|
||||
{host: props.host});
|
||||
const description2 = _t("If the other version of Riot is still open in another tab, " +
|
||||
"please close it as using Riot on the same host with both " +
|
||||
"lazy loading enabled and disabled simultaneously will cause issues.");
|
||||
const brand = SdkConfig.get().brand;
|
||||
const description1 = _t(
|
||||
"You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " +
|
||||
"In this version lazy loading is disabled. " +
|
||||
"As the local cache is not compatible between these two settings, " +
|
||||
"%(brand)s needs to resync your account.",
|
||||
{
|
||||
brand,
|
||||
host: props.host,
|
||||
},
|
||||
);
|
||||
const description2 = _t(
|
||||
"If the other version of %(brand)s is still open in another tab, " +
|
||||
"please close it as using %(brand)s on the same host with both " +
|
||||
"lazy loading enabled and disabled simultaneously will cause issues.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
);
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,15 +18,21 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import QuestionDialog from './QuestionDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
||||
export default (props) => {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const description =
|
||||
_t("Riot now uses 3-5x less memory, by only loading information about other users"
|
||||
+ " when needed. Please wait whilst we resynchronise with the server!");
|
||||
_t(
|
||||
"%(brand)s now uses 3-5x less memory, by only loading information " +
|
||||
"about other users when needed. Please wait whilst we resynchronise " +
|
||||
"with the server!",
|
||||
{ brand },
|
||||
);
|
||||
|
||||
return (<QuestionDialog
|
||||
hasCancelButton={false}
|
||||
title={_t("Updating Riot")}
|
||||
title={_t("Updating %(brand)s", { brand })}
|
||||
description={<div>{description}</div>}
|
||||
button={_t("OK")}
|
||||
onFinished={props.onFinished}
|
||||
|
|
|
@ -18,10 +18,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class LogoutDialog extends React.Component {
|
||||
defaultProps = {
|
||||
|
@ -36,8 +35,8 @@ export default class LogoutDialog extends React.Component {
|
|||
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
|
||||
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
|
||||
|
||||
const lowBandwidth = SettingsStore.getValue("lowBandwidth");
|
||||
const shouldLoadBackupStatus = !lowBandwidth && !MatrixClientPeg.get().getKeyBackupEnabled();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
|
||||
|
||||
this.state = {
|
||||
shouldLoadBackupStatus: shouldLoadBackupStatus,
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class ManualDeviceKeyVerificationDialog extends React.Component {
|
||||
static propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
_onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onLegacyFinished = (confirm) => {
|
||||
if (confirm) {
|
||||
MatrixClientPeg.get().setDeviceVerified(
|
||||
this.props.userId, this.props.device.deviceId, true,
|
||||
);
|
||||
}
|
||||
this.props.onFinished(confirm);
|
||||
}
|
||||
|
||||
render() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
let text;
|
||||
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
|
||||
text = _t("Confirm by comparing the following with the User Settings in your other session:");
|
||||
} else {
|
||||
text = _t("Confirm this user's session by comparing the following with their User Settings:");
|
||||
}
|
||||
|
||||
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
|
||||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{ text }
|
||||
</p>
|
||||
<div className="mx_DeviceVerifyDialog_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{ _t("If they don't match, the security of your communication may be compromised.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<QuestionDialog
|
||||
title={_t("Verify session")}
|
||||
description={body}
|
||||
button={_t("Verify session")}
|
||||
onFinished={this._onLegacyFinished}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -54,17 +54,18 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
onContinueClick = () => {
|
||||
const { userId, device } = this.props;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const request = await cli.requestVerification(
|
||||
const requestPromise = cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
|
||||
this.props.onFinished(true);
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequest: request,
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ export default createReactClass({
|
|||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
fixedWidth: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -42,6 +44,7 @@ export default createReactClass({
|
|||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
quitOnly: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -61,11 +64,14 @@ export default createReactClass({
|
|||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
<BaseDialog
|
||||
className="mx_QuestionDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
contentId='mx_Dialog_content'
|
||||
headerImage={this.props.headerImage}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
fixedWidth={this.props.fixedWidth}
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
{ this.props.description }
|
||||
|
@ -73,7 +79,7 @@ export default createReactClass({
|
|||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
|
|
116
src/components/views/dialogs/RebrandDialog.tsx
Normal file
116
src/components/views/dialogs/RebrandDialog.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import DialogButtons from '../elements/DialogButtons';
|
||||
|
||||
export enum RebrandDialogKind {
|
||||
NAG,
|
||||
ONE_TIME,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
onFinished: (bool) => void;
|
||||
kind: RebrandDialogKind;
|
||||
targetUrl?: string;
|
||||
}
|
||||
|
||||
export default class RebrandDialog extends React.PureComponent<IProps> {
|
||||
private onDoneClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
private onGoToElementClick = () => {
|
||||
this.props.onFinished(true);
|
||||
};
|
||||
|
||||
private onRemindMeLaterClick = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private getPrettyTargetUrl() {
|
||||
const u = new URL(this.props.targetUrl);
|
||||
let ret = u.host;
|
||||
if (u.pathname !== '/') ret += u.pathname;
|
||||
return ret;
|
||||
}
|
||||
|
||||
getBodyText() {
|
||||
if (this.props.kind === RebrandDialogKind.NAG) {
|
||||
return _t(
|
||||
"Use your account to sign in to the latest version of the app at <a />", {},
|
||||
{
|
||||
a: sub => <a href={this.props.targetUrl} rel="noopener noreferrer" target="_blank">{this.getPrettyTargetUrl()}</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _t(
|
||||
"You’re already signed in and good to go here, but you can also grab the latest " +
|
||||
"versions of the app on all platforms at <a>element.io/get-started</a>.", {},
|
||||
{
|
||||
a: sub => <a href="https://element.io/get-started" rel="noopener noreferrer" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getDialogButtons() {
|
||||
if (this.props.kind === RebrandDialogKind.NAG) {
|
||||
return <DialogButtons primaryButton={_t("Go to Element")}
|
||||
primaryButtonClass='primary'
|
||||
onPrimaryButtonClick={this.onGoToElementClick}
|
||||
hasCancel={true}
|
||||
cancelButton={"Remind me later"}
|
||||
onCancel={this.onRemindMeLaterClick}
|
||||
focus={true}
|
||||
/>;
|
||||
} else {
|
||||
return <DialogButtons primaryButton={_t("Done")}
|
||||
primaryButtonClass='primary'
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this.onDoneClick}
|
||||
focus={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <BaseDialog title={_t("We’re excited to announce Riot is now Element!")}
|
||||
className='mx_RebrandDialog'
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
hasCancel={false}
|
||||
>
|
||||
<div className="mx_RebrandDialog_body">{this.getBodyText()}</div>
|
||||
<div className="mx_RebrandDialog_logoContainer">
|
||||
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/riot-logo.svg")} alt="Riot Logo" />
|
||||
<span className="mx_RebrandDialog_chevron" />
|
||||
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/element-logo.svg")} alt="Element Logo" />
|
||||
</div>
|
||||
<div>
|
||||
{_t(
|
||||
"Learn more at <a>element.io/previously-riot</a>", {}, {
|
||||
a: sub => <a href="https://element.io/previously-riot" rel="noopener noreferrer" target="_blank">{sub}</a>,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
{this.getDialogButtons()}
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -123,7 +123,6 @@ export default class ReportEventDialog extends PureComponent {
|
|||
</p>
|
||||
{adminMessage}
|
||||
<Field
|
||||
id="mx_ReportEventDialog_reason"
|
||||
className="mx_ReportEventDialog_reason"
|
||||
element="textarea"
|
||||
label={_t("Reason")}
|
||||
|
|
|
@ -27,21 +27,28 @@ import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsT
|
|||
import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
||||
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||
export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB";
|
||||
export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB";
|
||||
|
||||
export default class RoomSettingsDialog extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
if (this._dispatcherRef) dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction = (payload) => {
|
||||
|
@ -56,28 +63,33 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
const tabs = [];
|
||||
|
||||
tabs.push(new Tab(
|
||||
ROOM_GENERAL_TAB,
|
||||
_td("General"),
|
||||
"mx_RoomSettingsDialog_settingsIcon",
|
||||
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
ROOM_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
"mx_RoomSettingsDialog_securityIcon",
|
||||
<SecurityRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
ROOM_ROLES_TAB,
|
||||
_td("Roles & Permissions"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
<RolesRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
tabs.push(new Tab(
|
||||
ROOM_NOTIFICATIONS_TAB,
|
||||
_td("Notifications"),
|
||||
"mx_RoomSettingsDialog_rolesIcon",
|
||||
"mx_RoomSettingsDialog_notificationsIcon",
|
||||
<NotificationSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
|
||||
tabs.push(new Tab(
|
||||
ROOM_BRIDGES_TAB,
|
||||
_td("Bridges"),
|
||||
"mx_RoomSettingsDialog_bridgesIcon",
|
||||
<BridgeSettingsTab roomId={this.props.roomId} />,
|
||||
|
@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component {
|
|||
}
|
||||
|
||||
tabs.push(new Tab(
|
||||
ROOM_ADVANCED_TAB,
|
||||
_td("Advanced"),
|
||||
"mx_RoomSettingsDialog_warningIcon",
|
||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
||||
|
|
|
@ -30,7 +30,7 @@ export default createReactClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: async function() {
|
||||
componentDidMount: async function() {
|
||||
const recommended = await this.props.room.getRecommendedVersion();
|
||||
this._targetVersion = recommended.version;
|
||||
this.setState({busy: false});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as sdk from "../../../index";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
@ -63,6 +64,7 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
|
@ -96,8 +98,11 @@ export default class RoomUpgradeWarningDialog extends React.Component {
|
|||
<p>
|
||||
{_t(
|
||||
"This usually only affects how the room is processed on the server. If you're " +
|
||||
"having problems with your Riot, please <a>report a bug</a>.",
|
||||
{}, {
|
||||
"having problems with your %(brand)s, please <a>report a bug</a>.",
|
||||
{
|
||||
brand,
|
||||
},
|
||||
{
|
||||
"a": (sub) => {
|
||||
return <a href='#' onClick={this._openBugReportDialog}>{sub}</a>;
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -56,6 +57,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
|
@ -94,9 +96,10 @@ export default createReactClass({
|
|||
<p>{ _t("We encountered an error trying to restore your previous session.") }</p>
|
||||
|
||||
<p>{ _t(
|
||||
"If you have previously used a more recent version of Riot, your session " +
|
||||
"If you have previously used a more recent version of %(brand)s, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.",
|
||||
{ brand },
|
||||
) }</p>
|
||||
|
||||
<p>{ _t(
|
||||
|
|
|
@ -62,6 +62,7 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._input_value = createRef();
|
||||
this._uiAuth = createRef();
|
||||
|
|
|
@ -75,8 +75,8 @@ export default createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
console.info('SetPasswordDialog component will mount');
|
||||
componentDidMount: function() {
|
||||
console.info('SetPasswordDialog component did mount');
|
||||
},
|
||||
|
||||
_onPasswordChanged: function(res) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue