Merge pull request #6787 from SimonBrandner/task/structures-ts
This commit is contained in:
commit
3b39ba6590
12 changed files with 316 additions and 284 deletions
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
|
@ -26,38 +25,43 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
export default class EmbeddedPage extends React.PureComponent {
|
interface IProps {
|
||||||
static propTypes = {
|
// URL to request embedded page content from
|
||||||
// URL to request embedded page content from
|
url?: string;
|
||||||
url: PropTypes.string,
|
// Class name prefix to apply for a given instance
|
||||||
// Class name prefix to apply for a given instance
|
className?: string;
|
||||||
className: PropTypes.string,
|
// Whether to wrap the page in a scrollbar
|
||||||
// Whether to wrap the page in a scrollbar
|
scrollbar?: boolean;
|
||||||
scrollbar: PropTypes.bool,
|
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
||||||
// Map of keys to replace with values, e.g {$placeholder: "value"}
|
replaceMap?: Map<string, string>;
|
||||||
replaceMap: PropTypes.object,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
interface IState {
|
||||||
|
page: string;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props, context) {
|
export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||||
|
public static contextType = MatrixClientContext;
|
||||||
|
private unmounted = false;
|
||||||
|
private dispatcherRef: string = null;
|
||||||
|
|
||||||
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._dispatcherRef = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
page: '',
|
page: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(s) {
|
private translate(s: string): string {
|
||||||
// default implementation - skins may wish to extend this
|
// default implementation - skins may wish to extend this
|
||||||
return sanitizeHtml(_t(s));
|
return sanitizeHtml(_t(s));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._unmounted = false;
|
this.unmounted = false;
|
||||||
|
|
||||||
if (!this.props.url) {
|
if (!this.props.url) {
|
||||||
return;
|
return;
|
||||||
|
@ -70,7 +74,7 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
request(
|
request(
|
||||||
{ method: "GET", url: this.props.url },
|
{ method: "GET", url: this.props.url },
|
||||||
(err, response, body) => {
|
(err, response, body) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,22 +96,22 @@ export default class EmbeddedPage extends React.PureComponent {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this._dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
|
if (this.dispatcherRef !== null) dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
// HACK: Workaround for the context's MatrixClient not being set up at render time.
|
||||||
if (payload.action === 'client_started') {
|
if (payload.action === 'client_started') {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// HACK: Workaround for the context's MatrixClient not updating.
|
// HACK: Workaround for the context's MatrixClient not updating.
|
||||||
const client = this.context || MatrixClientPeg.get();
|
const client = this.context || MatrixClientPeg.get();
|
||||||
const isGuest = client ? client.isGuest() : true;
|
const isGuest = client ? client.isGuest() : true;
|
|
@ -15,16 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("structures.GenericErrorPage")
|
interface IProps {
|
||||||
export default class GenericErrorPage extends React.PureComponent {
|
title: React.ReactNode;
|
||||||
static propTypes = {
|
message: React.ReactNode;
|
||||||
title: PropTypes.object.isRequired, // jsx for title
|
}
|
||||||
message: PropTypes.object.isRequired, // jsx to display
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@replaceableComponent("structures.GenericErrorPage")
|
||||||
|
export default class GenericErrorPage extends React.PureComponent<IProps> {
|
||||||
render() {
|
render() {
|
||||||
return <div className='mx_GenericErrorPage'>
|
return <div className='mx_GenericErrorPage'>
|
||||||
<div className='mx_GenericErrorPage_box'>
|
<div className='mx_GenericErrorPage_box'>
|
|
@ -14,34 +14,39 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { createRef } from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
||||||
|
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
||||||
|
// by the parent element.
|
||||||
|
trackHorizontalOverflow?: boolean;
|
||||||
|
|
||||||
|
// If true, when the user tries to use their mouse wheel in the component it will
|
||||||
|
// scroll horizontally rather than vertically. This should only be used on components
|
||||||
|
// with no vertical scroll opportunity.
|
||||||
|
verticalScrollsHorizontally?: boolean;
|
||||||
|
|
||||||
|
children: React.ReactNode;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
leftIndicatorOffset: number | string;
|
||||||
|
rightIndicatorOffset: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.IndicatorScrollbar")
|
@replaceableComponent("structures.IndicatorScrollbar")
|
||||||
export default class IndicatorScrollbar extends React.Component {
|
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private autoHideScrollbar = createRef<AutoHideScrollbar>();
|
||||||
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
|
private scrollElement: HTMLDivElement;
|
||||||
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
|
private likelyTrackpadUser: boolean = null;
|
||||||
// by the parent element.
|
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
||||||
trackHorizontalOverflow: PropTypes.bool,
|
|
||||||
|
|
||||||
// If true, when the user tries to use their mouse wheel in the component it will
|
constructor(props: IProps) {
|
||||||
// scroll horizontally rather than vertically. This should only be used on components
|
|
||||||
// with no vertical scroll opportunity.
|
|
||||||
verticalScrollsHorizontally: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
this._collectScroller = this._collectScroller.bind(this);
|
|
||||||
this._collectScrollerComponent = this._collectScrollerComponent.bind(this);
|
|
||||||
this.checkOverflow = this.checkOverflow.bind(this);
|
|
||||||
this._scrollElement = null;
|
|
||||||
this._autoHideScrollbar = null;
|
|
||||||
this._likelyTrackpadUser = null;
|
|
||||||
this._checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
leftIndicatorOffset: 0,
|
leftIndicatorOffset: 0,
|
||||||
|
@ -49,30 +54,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToOrigin() {
|
private collectScroller = (scroller: HTMLDivElement): void => {
|
||||||
if (!this._scrollElement) return;
|
if (scroller && !this.scrollElement) {
|
||||||
|
this.scrollElement = scroller;
|
||||||
this._scrollElement.scrollLeft = 0;
|
|
||||||
this._scrollElement.scrollTop = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_collectScroller(scroller) {
|
|
||||||
if (scroller && !this._scrollElement) {
|
|
||||||
this._scrollElement = scroller;
|
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
this.scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_collectScrollerComponent(autoHideScrollbar) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
this._autoHideScrollbar = autoHideScrollbar;
|
const prevLen = React.Children.count(prevProps.children);
|
||||||
}
|
const curLen = React.Children.count(this.props.children);
|
||||||
|
|
||||||
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.
|
// check overflow only if amount of children changes.
|
||||||
// if we don't guard here, we end up with an infinite
|
// if we don't guard here, we end up with an infinite
|
||||||
// render > componentDidUpdate > checkOverflow > setState > render loop
|
// render > componentDidUpdate > checkOverflow > setState > render loop
|
||||||
|
@ -81,62 +75,58 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.checkOverflow();
|
this.checkOverflow();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOverflow() {
|
private checkOverflow = (): void => {
|
||||||
const hasTopOverflow = this._scrollElement.scrollTop > 0;
|
const hasTopOverflow = this.scrollElement.scrollTop > 0;
|
||||||
const hasBottomOverflow = this._scrollElement.scrollHeight >
|
const hasBottomOverflow = this.scrollElement.scrollHeight >
|
||||||
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
|
(this.scrollElement.scrollTop + this.scrollElement.clientHeight);
|
||||||
const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
|
const hasLeftOverflow = this.scrollElement.scrollLeft > 0;
|
||||||
const hasRightOverflow = this._scrollElement.scrollWidth >
|
const hasRightOverflow = this.scrollElement.scrollWidth >
|
||||||
(this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
|
(this.scrollElement.scrollLeft + this.scrollElement.clientWidth);
|
||||||
|
|
||||||
if (hasTopOverflow) {
|
if (hasTopOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
|
||||||
}
|
}
|
||||||
if (hasBottomOverflow) {
|
if (hasBottomOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
|
||||||
}
|
}
|
||||||
if (hasLeftOverflow) {
|
if (hasLeftOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
|
||||||
}
|
}
|
||||||
if (hasRightOverflow) {
|
if (hasRightOverflow) {
|
||||||
this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
|
||||||
} else {
|
} else {
|
||||||
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
this.scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.trackHorizontalOverflow) {
|
if (this.props.trackHorizontalOverflow) {
|
||||||
this.setState({
|
this.setState({
|
||||||
// Offset from absolute position of the container
|
// Offset from absolute position of the container
|
||||||
leftIndicatorOffset: hasLeftOverflow ? `${this._scrollElement.scrollLeft}px` : '0',
|
leftIndicatorOffset: hasLeftOverflow ? `${this.scrollElement.scrollLeft}px` : '0',
|
||||||
|
|
||||||
// Negative because we're coming from the right
|
// Negative because we're coming from the right
|
||||||
rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
|
rightIndicatorOffset: hasRightOverflow ? `-${this.scrollElement.scrollLeft}px` : '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
getScrollTop() {
|
public componentWillUnmount(): void {
|
||||||
return this._autoHideScrollbar.getScrollTop();
|
if (this.scrollElement) {
|
||||||
}
|
this.scrollElement.removeEventListener("scroll", this.checkOverflow);
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._scrollElement) {
|
|
||||||
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseWheel = (e) => {
|
private onMouseWheel = (e: React.WheelEvent): void => {
|
||||||
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
|
if (this.props.verticalScrollsHorizontally && this.scrollElement) {
|
||||||
// xyThreshold is the amount of horizontal motion required for the component to
|
// xyThreshold is the amount of horizontal motion required for the component to
|
||||||
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
// ignore the vertical delta in a scroll. Used to stop trackpads from acting in
|
||||||
// strange ways. Should be positive.
|
// strange ways. Should be positive.
|
||||||
|
@ -150,19 +140,19 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
// for at least the next 1 minute.
|
// for at least the next 1 minute.
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (Math.abs(e.deltaX) > 0) {
|
if (Math.abs(e.deltaX) > 0) {
|
||||||
this._likelyTrackpadUser = true;
|
this.likelyTrackpadUser = true;
|
||||||
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
|
this.checkAgainForTrackpad = now + (1 * 60 * 1000);
|
||||||
} else {
|
} else {
|
||||||
// if we haven't seen any horizontal scrolling for a while, assume
|
// if we haven't seen any horizontal scrolling for a while, assume
|
||||||
// the user might have plugged in a mousewheel
|
// the user might have plugged in a mousewheel
|
||||||
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
|
if (this.likelyTrackpadUser && now >= this.checkAgainForTrackpad) {
|
||||||
this._likelyTrackpadUser = false;
|
this.likelyTrackpadUser = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't mess with the horizontal scroll for trackpad users
|
// don't mess with the horizontal scroll for trackpad users
|
||||||
// See https://github.com/vector-im/element-web/issues/10005
|
// See https://github.com/vector-im/element-web/issues/10005
|
||||||
if (this._likelyTrackpadUser) {
|
if (this.likelyTrackpadUser) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,13 +168,13 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
|
|
||||||
// noinspection JSSuspiciousNameCombination
|
// noinspection JSSuspiciousNameCombination
|
||||||
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
|
||||||
this._scrollElement.scrollLeft += val * yRetention;
|
this.scrollElement.scrollLeft += val * yRetention;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||||
|
|
||||||
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
const leftIndicatorStyle = { left: this.state.leftIndicatorOffset };
|
||||||
|
@ -195,8 +185,8 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this.autoHideScrollbar}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this.collectScroller}
|
||||||
onWheel={this.onMouseWheel}
|
onWheel={this.onMouseWheel}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
|
@ -16,25 +16,35 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Resizable } from 're-resizable';
|
import { NumberSize, Resizable } from 're-resizable';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
import { Direction } from "re-resizable/lib/resizer";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
collapsedRhs?: boolean;
|
||||||
|
panel: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.MainSplit")
|
@replaceableComponent("structures.MainSplit")
|
||||||
export default class MainSplit extends React.Component {
|
export default class MainSplit extends React.Component<IProps> {
|
||||||
_onResizeStart = () => {
|
private onResizeStart = (): void => {
|
||||||
this.props.resizeNotifier.startResizing();
|
this.props.resizeNotifier.startResizing();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResize = () => {
|
private onResize = (): void => {
|
||||||
this.props.resizeNotifier.notifyRightHandleResized();
|
this.props.resizeNotifier.notifyRightHandleResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResizeStop = (event, direction, refToElement, delta) => {
|
private onResizeStop = (
|
||||||
|
event: MouseEvent | TouchEvent, direction: Direction, elementRef: HTMLElement, delta: NumberSize,
|
||||||
|
): void => {
|
||||||
this.props.resizeNotifier.stopResizing();
|
this.props.resizeNotifier.stopResizing();
|
||||||
window.localStorage.setItem("mx_rhs_size", this._loadSidePanelSize().width + delta.width);
|
window.localStorage.setItem("mx_rhs_size", (this.loadSidePanelSize().width + delta.width).toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
_loadSidePanelSize() {
|
private loadSidePanelSize(): {height: string | number, width: number} {
|
||||||
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10);
|
||||||
|
|
||||||
if (isNaN(rhsSize)) {
|
if (isNaN(rhsSize)) {
|
||||||
|
@ -47,7 +57,7 @@ export default class MainSplit extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const bodyView = React.Children.only(this.props.children);
|
const bodyView = React.Children.only(this.props.children);
|
||||||
const panelView = this.props.panel;
|
const panelView = this.props.panel;
|
||||||
|
|
||||||
|
@ -56,7 +66,7 @@ export default class MainSplit extends React.Component {
|
||||||
let children;
|
let children;
|
||||||
if (hasResizer) {
|
if (hasResizer) {
|
||||||
children = <Resizable
|
children = <Resizable
|
||||||
defaultSize={this._loadSidePanelSize()}
|
defaultSize={this.loadSidePanelSize()}
|
||||||
minWidth={264}
|
minWidth={264}
|
||||||
maxWidth="50%"
|
maxWidth="50%"
|
||||||
enable={{
|
enable={{
|
||||||
|
@ -69,9 +79,9 @@ export default class MainSplit extends React.Component {
|
||||||
bottomLeft: false,
|
bottomLeft: false,
|
||||||
topLeft: false,
|
topLeft: false,
|
||||||
}}
|
}}
|
||||||
onResizeStart={this._onResizeStart}
|
onResizeStart={this.onResizeStart}
|
||||||
onResize={this._onResize}
|
onResize={this.onResize}
|
||||||
onResizeStop={this._onResizeStop}
|
onResizeStop={this.onResizeStop}
|
||||||
className="mx_RightPanel_ResizeWrapper"
|
className="mx_RightPanel_ResizeWrapper"
|
||||||
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
|
||||||
>
|
>
|
|
@ -15,95 +15,110 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t, _td } from '../../languageHandler';
|
import { _t, _td } from '../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
|
||||||
import Resend from '../../Resend';
|
import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { EventStatus } from "matrix-js-sdk/src/models/event";
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync.api";
|
||||||
|
import { ISyncStateData } from "matrix-js-sdk/src/sync";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
export function getUnsentMessages(room) {
|
export function getUnsentMessages(room: Room): MatrixEvent[] {
|
||||||
if (!room) { return []; }
|
if (!room) { return []; }
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
return ev.status === EventStatus.NOT_SENT;
|
return ev.status === EventStatus.NOT_SENT;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// the room this statusbar is representing.
|
||||||
|
room: Room;
|
||||||
|
|
||||||
|
// true if the room is being peeked at. This affects components that shouldn't
|
||||||
|
// logically be shown when peeking, such as a prompt to invite people to a room.
|
||||||
|
isPeeking?: boolean;
|
||||||
|
// callback for when the user clicks on the 'resend all' button in the
|
||||||
|
// 'unsent messages' bar
|
||||||
|
onResendAllClick?: () => void;
|
||||||
|
|
||||||
|
// callback for when the user clicks on the 'cancel all' button in the
|
||||||
|
// 'unsent messages' bar
|
||||||
|
onCancelAllClick?: () => void;
|
||||||
|
|
||||||
|
// callback for when the user clicks on the 'invite others' button in the
|
||||||
|
// 'you are alone' bar
|
||||||
|
onInviteClick?: () => void;
|
||||||
|
|
||||||
|
// callback for when we do something that changes the size of the
|
||||||
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
|
// component.
|
||||||
|
onResize?: () => void;
|
||||||
|
|
||||||
|
// callback for when the status bar can be hidden from view, as it is
|
||||||
|
// not displaying anything
|
||||||
|
onHidden?: () => void;
|
||||||
|
|
||||||
|
// callback for when the status bar is displaying something and should
|
||||||
|
// be visible
|
||||||
|
onVisible?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
syncState: SyncState;
|
||||||
|
syncStateData: ISyncStateData;
|
||||||
|
unsentMessages: MatrixEvent[];
|
||||||
|
isResending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomStatusBar")
|
@replaceableComponent("structures.RoomStatusBar")
|
||||||
export default class RoomStatusBar extends React.PureComponent {
|
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
public static contextType = MatrixClientContext;
|
||||||
// the room this statusbar is representing.
|
|
||||||
room: PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
// true if the room is being peeked at. This affects components that shouldn't
|
constructor(props: IProps, context: typeof MatrixClientContext) {
|
||||||
// logically be shown when peeking, such as a prompt to invite people to a room.
|
super(props, context);
|
||||||
isPeeking: PropTypes.bool,
|
|
||||||
|
|
||||||
// callback for when the user clicks on the 'resend all' button in the
|
this.state = {
|
||||||
// 'unsent messages' bar
|
syncState: this.context.getSyncState(),
|
||||||
onResendAllClick: PropTypes.func,
|
syncStateData: this.context.getSyncStateData(),
|
||||||
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
// callback for when the user clicks on the 'cancel all' button in the
|
isResending: false,
|
||||||
// 'unsent messages' bar
|
};
|
||||||
onCancelAllClick: PropTypes.func,
|
|
||||||
|
|
||||||
// callback for when the user clicks on the 'invite others' button in the
|
|
||||||
// 'you are alone' bar
|
|
||||||
onInviteClick: PropTypes.func,
|
|
||||||
|
|
||||||
// callback for when we do something that changes the size of the
|
|
||||||
// status bar. This is used to trigger a re-layout in the parent
|
|
||||||
// component.
|
|
||||||
onResize: PropTypes.func,
|
|
||||||
|
|
||||||
// callback for when the status bar can be hidden from view, as it is
|
|
||||||
// not displaying anything
|
|
||||||
onHidden: PropTypes.func,
|
|
||||||
|
|
||||||
// callback for when the status bar is displaying something and should
|
|
||||||
// be visible
|
|
||||||
onVisible: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
|
||||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
|
||||||
unsentMessages: getUnsentMessages(this.props.room),
|
|
||||||
isResending: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
|
||||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
|
||||||
|
|
||||||
this._checkSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidMount(): void {
|
||||||
this._checkSize();
|
const client = this.context;
|
||||||
|
client.on("sync", this.onSyncStateChange);
|
||||||
|
client.on("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
|
|
||||||
|
this.checkSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidUpdate(): void {
|
||||||
|
this.checkSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
const client = MatrixClientPeg.get();
|
const client = this.context;
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("sync", this.onSyncStateChange);
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
client.removeListener("Room.localEchoUpdated", this.onRoomLocalEchoUpdated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSyncStateChange = (state, prevState, data) => {
|
private onSyncStateChange = (state: SyncState, prevState: SyncState, data: ISyncStateData): void => {
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +128,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onResendAllClick = () => {
|
private onResendAllClick = (): void => {
|
||||||
Resend.resendUnsentEvents(this.props.room).then(() => {
|
Resend.resendUnsentEvents(this.props.room).then(() => {
|
||||||
this.setState({ isResending: false });
|
this.setState({ isResending: false });
|
||||||
});
|
});
|
||||||
|
@ -121,12 +136,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelAllClick = () => {
|
private onCancelAllClick = (): void => {
|
||||||
Resend.cancelUnsentEvents(this.props.room);
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
dis.fire(Action.FocusSendMessageComposer);
|
dis.fire(Action.FocusSendMessageComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
|
private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||||
if (room.roomId !== this.props.room.roomId) return;
|
if (room.roomId !== this.props.room.roomId) return;
|
||||||
const messages = getUnsentMessages(this.props.room);
|
const messages = getUnsentMessages(this.props.room);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -136,8 +151,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize() {
|
private checkSize(): void {
|
||||||
if (this._getSize()) {
|
if (this.getSize()) {
|
||||||
if (this.props.onVisible) this.props.onVisible();
|
if (this.props.onVisible) this.props.onVisible();
|
||||||
} else {
|
} else {
|
||||||
if (this.props.onHidden) this.props.onHidden();
|
if (this.props.onHidden) this.props.onHidden();
|
||||||
|
@ -147,8 +162,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
// We don't need the actual height - just whether it is likely to have
|
// We don't need the actual height - just whether it is likely to have
|
||||||
// changed - so we use '0' to indicate normal size, and other values to
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
// indicate other sizes.
|
// indicate other sizes.
|
||||||
_getSize() {
|
private getSize(): number {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
|
@ -156,7 +171,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldShowConnectionError() {
|
private shouldShowConnectionError(): boolean {
|
||||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||||
// a connection!
|
// a connection!
|
||||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||||
|
@ -164,12 +179,12 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
const errorIsMauError = Boolean(
|
const errorIsMauError = Boolean(
|
||||||
this.state.syncStateData &&
|
this.state.syncStateData &&
|
||||||
this.state.syncStateData.error &&
|
this.state.syncStateData.error &&
|
||||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED',
|
this.state.syncStateData.error.name === 'M_RESOURCE_LIMIT_EXCEEDED',
|
||||||
);
|
);
|
||||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getUnsentMessageContent() {
|
private getUnsentMessageContent(): JSX.Element {
|
||||||
const unsentMessages = this.state.unsentMessages;
|
const unsentMessages = this.state.unsentMessages;
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
@ -221,10 +236,10 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttonRow = <>
|
let buttonRow = <>
|
||||||
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
{ _t("Delete all") }
|
{ _t("Delete all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
||||||
{ _t("Retry all") }
|
{ _t("Retry all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
|
@ -260,8 +275,8 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this._shouldShowConnectionError()) {
|
if (this.shouldShowConnectionError()) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar">
|
<div className="mx_RoomStatusBar">
|
||||||
<div role="alert">
|
<div role="alert">
|
||||||
|
@ -287,7 +302,7 @@ export default class RoomStatusBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
|
||||||
return this._getUnsentMessageContent();
|
return this.getUnsentMessageContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
@ -24,106 +23,116 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onSearch?: (query: string) => void;
|
||||||
|
onCleared?: (source?: string) => void;
|
||||||
|
onKeyDown?: (ev: React.KeyboardEvent) => void;
|
||||||
|
onFocus?: (ev: React.FocusEvent) => void;
|
||||||
|
onBlur?: (ev: React.FocusEvent) => void;
|
||||||
|
className?: string;
|
||||||
|
placeholder: string;
|
||||||
|
blurredPlaceholder?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
initialValue?: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
|
||||||
|
// If true, the search box will focus and clear itself
|
||||||
|
// on room search focus action (it would be nicer to take
|
||||||
|
// this functionality out, but not obvious how that would work)
|
||||||
|
enableRoomSearchFocus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
searchTerm: string;
|
||||||
|
blurred: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.SearchBox")
|
@replaceableComponent("structures.SearchBox")
|
||||||
export default class SearchBox extends React.Component {
|
export default class SearchBox extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private dispatcherRef: string;
|
||||||
onSearch: PropTypes.func,
|
private search = createRef<HTMLInputElement>();
|
||||||
onCleared: PropTypes.func,
|
|
||||||
onKeyDown: PropTypes.func,
|
|
||||||
className: PropTypes.string,
|
|
||||||
placeholder: PropTypes.string.isRequired,
|
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
initialValue: PropTypes.string,
|
|
||||||
|
|
||||||
// If true, the search box will focus and clear itself
|
static defaultProps: Partial<IProps> = {
|
||||||
// on room search focus action (it would be nicer to take
|
|
||||||
// this functionality out, but not obvious how that would work)
|
|
||||||
enableRoomSearchFocus: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
enableRoomSearchFocus: false,
|
enableRoomSearchFocus: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._search = createRef();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchTerm: this.props.initialValue || "",
|
searchTerm: props.initialValue || "",
|
||||||
blurred: true,
|
blurred: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = payload => {
|
private onAction = (payload): void => {
|
||||||
if (!this.props.enableRoomSearchFocus) return;
|
if (!this.props.enableRoomSearchFocus) return;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
if (this._search.current && payload.clear_search) {
|
if (this.search.current && payload.clear_search) {
|
||||||
this._clearSearch();
|
this.clearSearch();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'focus_room_filter':
|
case 'focus_room_filter':
|
||||||
if (this._search.current) {
|
if (this.search.current) {
|
||||||
this._search.current.focus();
|
this.search.current.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = () => {
|
private onChange = (): void => {
|
||||||
if (!this._search.current) return;
|
if (!this.search.current) return;
|
||||||
this.setState({ searchTerm: this._search.current.value });
|
this.setState({ searchTerm: this.search.current.value });
|
||||||
this.onSearch();
|
this.onSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearch = throttle(() => {
|
private onSearch = throttle((): void => {
|
||||||
this.props.onSearch(this._search.current.value);
|
this.props.onSearch(this.search.current.value);
|
||||||
}, 200, { trailing: true, leading: true });
|
}, 200, { trailing: true, leading: true });
|
||||||
|
|
||||||
_onKeyDown = ev => {
|
private onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ESCAPE:
|
case Key.ESCAPE:
|
||||||
this._clearSearch("keyboard");
|
this.clearSearch("keyboard");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
if (this.props.onKeyDown) this.props.onKeyDown(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onFocus = ev => {
|
private onFocus = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: false });
|
this.setState({ blurred: false });
|
||||||
ev.target.select();
|
(ev.target as HTMLInputElement).select();
|
||||||
if (this.props.onFocus) {
|
if (this.props.onFocus) {
|
||||||
this.props.onFocus(ev);
|
this.props.onFocus(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onBlur = ev => {
|
private onBlur = (ev: React.FocusEvent): void => {
|
||||||
this.setState({ blurred: true });
|
this.setState({ blurred: true });
|
||||||
if (this.props.onBlur) {
|
if (this.props.onBlur) {
|
||||||
this.props.onBlur(ev);
|
this.props.onBlur(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_clearSearch(source) {
|
private clearSearch(source?: string): void {
|
||||||
this._search.current.value = "";
|
this.search.current.value = "";
|
||||||
this.onChange();
|
this.onChange();
|
||||||
if (this.props.onCleared) {
|
if (this.props.onCleared) {
|
||||||
this.props.onCleared(source);
|
this.props.onCleared(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
// check for collapsed here and
|
// check for collapsed here and
|
||||||
// not at parent so we keep
|
// not at parent so we keep
|
||||||
// searchTerm in our state
|
// searchTerm in our state
|
||||||
|
@ -136,7 +145,7 @@ export default class SearchBox extends React.Component {
|
||||||
key="button"
|
key="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="mx_SearchBox_closeButton"
|
className="mx_SearchBox_closeButton"
|
||||||
onClick={() => {this._clearSearch("button"); }}
|
onClick={() => {this.clearSearch("button"); }}
|
||||||
/>) : undefined;
|
/>) : undefined;
|
||||||
|
|
||||||
// show a shorter placeholder when blurred, if requested
|
// show a shorter placeholder when blurred, if requested
|
||||||
|
@ -151,13 +160,13 @@ export default class SearchBox extends React.Component {
|
||||||
<input
|
<input
|
||||||
key="searchfield"
|
key="searchfield"
|
||||||
type="text"
|
type="text"
|
||||||
ref={this._search}
|
ref={this.search}
|
||||||
className={"mx_textinput_icon mx_textinput_search " + className}
|
className={"mx_textinput_icon mx_textinput_search " + className}
|
||||||
value={this.state.searchTerm}
|
value={this.state.searchTerm}
|
||||||
onFocus={this._onFocus}
|
onFocus={this.onFocus}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onBlur={this._onBlur}
|
onBlur={this.onBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={this.props.autoFocus}
|
autoFocus={this.props.autoFocus}
|
|
@ -16,52 +16,60 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import Modal from '../../Modal';
|
import Modal from '../../Modal';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import HomePage from "./HomePage";
|
import HomePage from "./HomePage";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import MainSplit from "./MainSplit";
|
||||||
|
import RightPanel from "./RightPanel";
|
||||||
|
import Spinner from "../views/elements/Spinner";
|
||||||
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
userId?: string;
|
||||||
|
resizeNotifier: ResizeNotifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
member?: RoomMember;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.UserView")
|
@replaceableComponent("structures.UserView")
|
||||||
export default class UserView extends React.Component {
|
export default class UserView extends React.Component<IProps, IState> {
|
||||||
static get propTypes() {
|
constructor(props: IProps) {
|
||||||
return {
|
super(props);
|
||||||
userId: PropTypes.string,
|
this.state = {
|
||||||
|
loading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
public componentDidMount(): void {
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.userId) {
|
if (this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
public componentDidUpdate(prevProps: IProps): void {
|
||||||
// XXX: We shouldn't need to null check the userId here, but we declare
|
// 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
|
// it as optional and MatrixChat sometimes fires in a way which results
|
||||||
// in an NPE when we try to update the profile info.
|
// in an NPE when we try to update the profile info.
|
||||||
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
if (prevProps.userId !== this.props.userId && this.props.userId) {
|
||||||
this._loadProfileInfo();
|
this.loadProfileInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadProfileInfo() {
|
private async loadProfileInfo(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
let profileInfo;
|
let profileInfo;
|
||||||
try {
|
try {
|
||||||
profileInfo = await cli.getProfileInfo(this.props.userId);
|
profileInfo = await cli.getProfileInfo(this.props.userId);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
|
||||||
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
|
||||||
title: _t('Could not load user profile'),
|
title: _t('Could not load user profile'),
|
||||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||||
|
@ -75,14 +83,11 @@ export default class UserView extends React.Component {
|
||||||
this.setState({ member, loading: false });
|
this.setState({ member, loading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else if (this.state.member) {
|
} else if (this.state.member?.user) {
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />;
|
||||||
const MainSplit = sdk.getComponent('structures.MainSplit');
|
|
||||||
const panel = <RightPanel user={this.state.member} />;
|
|
||||||
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<HomePage />
|
<HomePage />
|
||||||
</MainSplit>);
|
</MainSplit>);
|
|
@ -17,24 +17,28 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import * as sdk from "../../index";
|
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
|
||||||
import { canEditContent } from "../../utils/EventUtils";
|
import { canEditContent } from "../../utils/EventUtils";
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { IDialogProps } from "../views/dialogs/IDialogProps";
|
||||||
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
mxEvent: MatrixEvent; // the MatrixEvent associated with the context menu
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isEditing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.ViewSource")
|
@replaceableComponent("structures.ViewSource")
|
||||||
export default class ViewSource extends React.Component {
|
export default class ViewSource extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -42,19 +46,20 @@ export default class ViewSource extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack() {
|
private onBack(): void {
|
||||||
// TODO: refresh the "Event ID:" modal header
|
// TODO: refresh the "Event ID:" modal header
|
||||||
this.setState({ isEditing: false });
|
this.setState({ isEditing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onEdit() {
|
private onEdit(): void {
|
||||||
this.setState({ isEditing: true });
|
this.setState({ isEditing: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the dialog body for viewing the event source
|
// returns the dialog body for viewing the event source
|
||||||
viewSourceContent() {
|
private viewSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
|
// @ts-ignore
|
||||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||||
const originalEventSource = mxEvent.event;
|
const originalEventSource = mxEvent.event;
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the id of the initial message, not the id of the previous edit
|
// returns the id of the initial message, not the id of the previous edit
|
||||||
getBaseEventId() {
|
private getBaseEventId(): string {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
const baseMxEvent = this.props.mxEvent;
|
const baseMxEvent = this.props.mxEvent;
|
||||||
|
@ -100,7 +105,7 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the SendCustomEvent component prefilled with the correct details
|
// returns the SendCustomEvent component prefilled with the correct details
|
||||||
editSourceContent() {
|
private editSourceContent(): JSX.Element {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isStateEvent = mxEvent.isState();
|
const isStateEvent = mxEvent.isState();
|
||||||
|
@ -159,14 +164,13 @@ export default class ViewSource extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canSendStateEvent(mxEvent) {
|
private canSendStateEvent(mxEvent: MatrixEvent): boolean {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(mxEvent.getRoomId());
|
const room = cli.getRoom(mxEvent.getRoomId());
|
||||||
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
|
|
||||||
const isEditing = this.state.isEditing;
|
const isEditing = this.state.isEditing;
|
|
@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
<AutoHideScrollbar className="mx_AddExistingToSpace_content">
|
||||||
|
|
|
@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search for rooms or people")}
|
placeholder={_t("Search for rooms or people")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
|
|
|
@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={filterPlaceholder}
|
placeholder={filterPlaceholder}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">
|
||||||
|
|
|
@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
placeholder={_t("Search spaces")}
|
placeholder={_t("Search spaces")}
|
||||||
onSearch={setQuery}
|
onSearch={setQuery}
|
||||||
autoComplete={true}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">
|
||||||
|
|
Loading…
Reference in a new issue