Merge pull request #6787 from SimonBrandner/task/structures-ts

This commit is contained in:
Michael Telatynski 2021-09-16 10:45:12 +01:00 committed by GitHub
commit 3b39ba6590
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 316 additions and 284 deletions

View file

@ -17,7 +17,6 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import request from 'browser-request';
import { _t } from '../../languageHandler';
import sanitizeHtml from 'sanitize-html';
@ -26,38 +25,43 @@ import { MatrixClientPeg } from '../../MatrixClientPeg';
import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { ActionPayload } from "../../dispatcher/payloads";
export default class EmbeddedPage extends React.PureComponent {
static propTypes = {
// URL to request embedded page content from
url: PropTypes.string,
// Class name prefix to apply for a given instance
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,
};
interface IProps {
// URL to request embedded page content from
url?: string;
// Class name prefix to apply for a given instance
className?: string;
// Whether to wrap the page in a scrollbar
scrollbar?: boolean;
// Map of keys to replace with values, e.g {$placeholder: "value"}
replaceMap?: Map<string, string>;
}
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);
this._dispatcherRef = null;
this.state = {
page: '',
};
}
translate(s) {
private translate(s: string): string {
// default implementation - skins may wish to extend this
return sanitizeHtml(_t(s));
}
componentDidMount() {
this._unmounted = false;
public componentDidMount(): void {
this.unmounted = false;
if (!this.props.url) {
return;
@ -70,7 +74,7 @@ export default class EmbeddedPage extends React.PureComponent {
request(
{ method: "GET", url: this.props.url },
(err, response, body) => {
if (this._unmounted) {
if (this.unmounted) {
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() {
this._unmounted = true;
if (this._dispatcherRef !== null) dis.unregister(this._dispatcherRef);
public componentWillUnmount(): void {
this.unmounted = true;
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.
if (payload.action === 'client_started') {
this.forceUpdate();
}
};
render() {
public render(): JSX.Element {
// HACK: Workaround for the context's MatrixClient not updating.
const client = this.context || MatrixClientPeg.get();
const isGuest = client ? client.isGuest() : true;

View file

@ -15,16 +15,15 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent {
static propTypes = {
title: PropTypes.object.isRequired, // jsx for title
message: PropTypes.object.isRequired, // jsx to display
};
interface IProps {
title: React.ReactNode;
message: React.ReactNode;
}
@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent<IProps> {
render() {
return <div className='mx_GenericErrorPage'>
<div className='mx_GenericErrorPage_box'>

View file

@ -14,34 +14,39 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import React, { createRef } from "react";
import AutoHideScrollbar from "./AutoHideScrollbar";
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")
export default class IndicatorScrollbar extends React.Component {
static propTypes = {
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
// by the parent element.
trackHorizontalOverflow: PropTypes.bool,
export default class IndicatorScrollbar extends React.Component<IProps, IState> {
private autoHideScrollbar = createRef<AutoHideScrollbar>();
private scrollElement: HTMLDivElement;
private likelyTrackpadUser: boolean = null;
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
// 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: PropTypes.bool,
};
constructor(props) {
constructor(props: IProps) {
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 = {
leftIndicatorOffset: 0,
@ -49,30 +54,19 @@ export default class IndicatorScrollbar extends React.Component {
};
}
moveToOrigin() {
if (!this._scrollElement) return;
this._scrollElement.scrollLeft = 0;
this._scrollElement.scrollTop = 0;
}
_collectScroller(scroller) {
if (scroller && !this._scrollElement) {
this._scrollElement = scroller;
private collectScroller = (scroller: HTMLDivElement): void => {
if (scroller && !this.scrollElement) {
this.scrollElement = scroller;
// 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
this._scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
this.scrollElement.addEventListener("scroll", this.checkOverflow, { passive: true });
this.checkOverflow();
}
}
};
_collectScrollerComponent(autoHideScrollbar) {
this._autoHideScrollbar = autoHideScrollbar;
}
componentDidUpdate(prevProps) {
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
const curLen = this.props.children && this.props.children.length || 0;
public componentDidUpdate(prevProps: IProps): void {
const prevLen = React.Children.count(prevProps.children);
const curLen = React.Children.count(this.props.children);
// 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
@ -81,62 +75,58 @@ export default class IndicatorScrollbar extends React.Component {
}
}
componentDidMount() {
public componentDidMount(): void {
this.checkOverflow();
}
checkOverflow() {
const hasTopOverflow = this._scrollElement.scrollTop > 0;
const hasBottomOverflow = this._scrollElement.scrollHeight >
(this._scrollElement.scrollTop + this._scrollElement.clientHeight);
const hasLeftOverflow = this._scrollElement.scrollLeft > 0;
const hasRightOverflow = this._scrollElement.scrollWidth >
(this._scrollElement.scrollLeft + this._scrollElement.clientWidth);
private checkOverflow = (): void => {
const hasTopOverflow = this.scrollElement.scrollTop > 0;
const hasBottomOverflow = this.scrollElement.scrollHeight >
(this.scrollElement.scrollTop + this.scrollElement.clientHeight);
const hasLeftOverflow = this.scrollElement.scrollLeft > 0;
const hasRightOverflow = this.scrollElement.scrollWidth >
(this.scrollElement.scrollLeft + this.scrollElement.clientWidth);
if (hasTopOverflow) {
this._scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
this.scrollElement.classList.add("mx_IndicatorScrollbar_topOverflow");
} else {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
this.scrollElement.classList.remove("mx_IndicatorScrollbar_topOverflow");
}
if (hasBottomOverflow) {
this._scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
this.scrollElement.classList.add("mx_IndicatorScrollbar_bottomOverflow");
} else {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
this.scrollElement.classList.remove("mx_IndicatorScrollbar_bottomOverflow");
}
if (hasLeftOverflow) {
this._scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
this.scrollElement.classList.add("mx_IndicatorScrollbar_leftOverflow");
} else {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
this.scrollElement.classList.remove("mx_IndicatorScrollbar_leftOverflow");
}
if (hasRightOverflow) {
this._scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
this.scrollElement.classList.add("mx_IndicatorScrollbar_rightOverflow");
} else {
this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
this.scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow");
}
if (this.props.trackHorizontalOverflow) {
this.setState({
// 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
rightIndicatorOffset: hasRightOverflow ? `-${this._scrollElement.scrollLeft}px` : '0',
rightIndicatorOffset: hasRightOverflow ? `-${this.scrollElement.scrollLeft}px` : '0',
});
}
}
};
getScrollTop() {
return this._autoHideScrollbar.getScrollTop();
}
componentWillUnmount() {
if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
public componentWillUnmount(): void {
if (this.scrollElement) {
this.scrollElement.removeEventListener("scroll", this.checkOverflow);
}
}
onMouseWheel = (e) => {
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
private onMouseWheel = (e: React.WheelEvent): void => {
if (this.props.verticalScrollsHorizontally && this.scrollElement) {
// 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
// strange ways. Should be positive.
@ -150,19 +140,19 @@ export default class IndicatorScrollbar extends React.Component {
// for at least the next 1 minute.
const now = new Date().getTime();
if (Math.abs(e.deltaX) > 0) {
this._likelyTrackpadUser = true;
this._checkAgainForTrackpad = now + (1 * 60 * 1000);
this.likelyTrackpadUser = true;
this.checkAgainForTrackpad = now + (1 * 60 * 1000);
} else {
// if we haven't seen any horizontal scrolling for a while, assume
// the user might have plugged in a mousewheel
if (this._likelyTrackpadUser && now >= this._checkAgainForTrackpad) {
this._likelyTrackpadUser = false;
if (this.likelyTrackpadUser && now >= this.checkAgainForTrackpad) {
this.likelyTrackpadUser = false;
}
}
// don't mess with the horizontal scroll for trackpad users
// See https://github.com/vector-im/element-web/issues/10005
if (this._likelyTrackpadUser) {
if (this.likelyTrackpadUser) {
return;
}
@ -178,13 +168,13 @@ export default class IndicatorScrollbar extends React.Component {
// noinspection JSSuspiciousNameCombination
const val = Math.abs(e.deltaY) < 25 ? (e.deltaY + additionalScroll) : e.deltaY;
this._scrollElement.scrollLeft += val * yRetention;
this.scrollElement.scrollLeft += val * yRetention;
}
}
};
render() {
// eslint-disable-next-line no-unused-vars
public render(): JSX.Element {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
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;
return (<AutoHideScrollbar
ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller}
ref={this.autoHideScrollbar}
wrappedRef={this.collectScroller}
onWheel={this.onMouseWheel}
{...otherProps}
>

View file

@ -16,25 +16,35 @@ limitations under the License.
*/
import React from 'react';
import { Resizable } from 're-resizable';
import { NumberSize, Resizable } from 're-resizable';
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")
export default class MainSplit extends React.Component {
_onResizeStart = () => {
export default class MainSplit extends React.Component<IProps> {
private onResizeStart = (): void => {
this.props.resizeNotifier.startResizing();
};
_onResize = () => {
private onResize = (): void => {
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();
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);
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 panelView = this.props.panel;
@ -56,7 +66,7 @@ export default class MainSplit extends React.Component {
let children;
if (hasResizer) {
children = <Resizable
defaultSize={this._loadSidePanelSize()}
defaultSize={this.loadSidePanelSize()}
minWidth={264}
maxWidth="50%"
enable={{
@ -69,9 +79,9 @@ export default class MainSplit extends React.Component {
bottomLeft: false,
topLeft: false,
}}
onResizeStart={this._onResizeStart}
onResize={this._onResize}
onResizeStop={this._onResizeStop}
onResizeStart={this.onResizeStart}
onResize={this.onResize}
onResizeStop={this.onResizeStop}
className="mx_RightPanel_ResizeWrapper"
handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
>

View file

@ -15,95 +15,110 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../languageHandler';
import { MatrixClientPeg } from '../../MatrixClientPeg';
import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
import { Action } from "../../dispatcher/actions";
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 { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
import AccessibleButton from "../views/elements/AccessibleButton";
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_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2;
export function getUnsentMessages(room) {
export function getUnsentMessages(room: Room): MatrixEvent[] {
if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) {
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")
export default class RoomStatusBar extends React.PureComponent {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext;
// 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: PropTypes.bool,
constructor(props: IProps, context: typeof MatrixClientContext) {
super(props, context);
// callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar
onResendAllClick: PropTypes.func,
// callback for when the user clicks on the 'cancel all' button in the
// '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();
this.state = {
syncState: this.context.getSyncState(),
syncStateData: this.context.getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
isResending: false,
};
}
componentDidUpdate() {
this._checkSize();
public componentDidMount(): void {
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...
const client = MatrixClientPeg.get();
const client = this.context;
if (client) {
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") {
return;
}
@ -113,7 +128,7 @@ export default class RoomStatusBar extends React.PureComponent {
});
};
_onResendAllClick = () => {
private onResendAllClick = (): void => {
Resend.resendUnsentEvents(this.props.room).then(() => {
this.setState({ isResending: false });
});
@ -121,12 +136,12 @@ export default class RoomStatusBar extends React.PureComponent {
dis.fire(Action.FocusSendMessageComposer);
};
_onCancelAllClick = () => {
private onCancelAllClick = (): void => {
Resend.cancelUnsentEvents(this.props.room);
dis.fire(Action.FocusSendMessageComposer);
};
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
private onRoomLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
if (room.roomId !== this.props.room.roomId) return;
const messages = getUnsentMessages(this.props.room);
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
_checkSize() {
if (this._getSize()) {
private checkSize(): void {
if (this.getSize()) {
if (this.props.onVisible) this.props.onVisible();
} else {
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
// changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes.
_getSize() {
if (this._shouldShowConnectionError()) {
private getSize(): number {
if (this.shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
return STATUS_BAR_EXPANDED_LARGE;
@ -156,7 +171,7 @@ export default class RoomStatusBar extends React.PureComponent {
return STATUS_BAR_HIDDEN;
}
_shouldShowConnectionError() {
private shouldShowConnectionError(): boolean {
// no conn bar trumps the "some not sent" msg since you can't resend without
// a connection!
// 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(
this.state.syncStateData &&
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;
}
_getUnsentMessageContent() {
private getUnsentMessageContent(): JSX.Element {
const unsentMessages = this.state.unsentMessages;
let title;
@ -221,10 +236,10 @@ export default class RoomStatusBar extends React.PureComponent {
}
let buttonRow = <>
<AccessibleButton onClick={this._onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
{ _t("Delete all") }
</AccessibleButton>
<AccessibleButton onClick={this._onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
{ _t("Retry all") }
</AccessibleButton>
</>;
@ -260,8 +275,8 @@ export default class RoomStatusBar extends React.PureComponent {
</>;
}
render() {
if (this._shouldShowConnectionError()) {
public render(): JSX.Element {
if (this.shouldShowConnectionError()) {
return (
<div className="mx_RoomStatusBar">
<div role="alert">
@ -287,7 +302,7 @@ export default class RoomStatusBar extends React.PureComponent {
}
if (this.state.unsentMessages.length > 0 || this.state.isResending) {
return this._getUnsentMessageContent();
return this.getUnsentMessageContent();
}
return null;

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher/dispatcher';
import { throttle } from 'lodash';
@ -24,106 +23,116 @@ import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
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")
export default class SearchBox extends React.Component {
static propTypes = {
onSearch: PropTypes.func,
onCleared: PropTypes.func,
onKeyDown: PropTypes.func,
className: PropTypes.string,
placeholder: PropTypes.string.isRequired,
autoFocus: PropTypes.bool,
initialValue: PropTypes.string,
export default class SearchBox extends React.Component<IProps, IState> {
private dispatcherRef: string;
private search = createRef<HTMLInputElement>();
// 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: PropTypes.bool,
};
static defaultProps = {
static defaultProps: Partial<IProps> = {
enableRoomSearchFocus: false,
};
constructor(props) {
constructor(props: IProps) {
super(props);
this._search = createRef();
this.state = {
searchTerm: this.props.initialValue || "",
searchTerm: props.initialValue || "",
blurred: true,
};
}
componentDidMount() {
public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef);
}
onAction = payload => {
private onAction = (payload): void => {
if (!this.props.enableRoomSearchFocus) return;
switch (payload.action) {
case 'view_room':
if (this._search.current && payload.clear_search) {
this._clearSearch();
if (this.search.current && payload.clear_search) {
this.clearSearch();
}
break;
case 'focus_room_filter':
if (this._search.current) {
this._search.current.focus();
if (this.search.current) {
this.search.current.focus();
}
break;
}
};
onChange = () => {
if (!this._search.current) return;
this.setState({ searchTerm: this._search.current.value });
private onChange = (): void => {
if (!this.search.current) return;
this.setState({ searchTerm: this.search.current.value });
this.onSearch();
};
onSearch = throttle(() => {
this.props.onSearch(this._search.current.value);
private onSearch = throttle((): void => {
this.props.onSearch(this.search.current.value);
}, 200, { trailing: true, leading: true });
_onKeyDown = ev => {
private onKeyDown = (ev: React.KeyboardEvent): void => {
switch (ev.key) {
case Key.ESCAPE:
this._clearSearch("keyboard");
this.clearSearch("keyboard");
break;
}
if (this.props.onKeyDown) this.props.onKeyDown(ev);
};
_onFocus = ev => {
private onFocus = (ev: React.FocusEvent): void => {
this.setState({ blurred: false });
ev.target.select();
(ev.target as HTMLInputElement).select();
if (this.props.onFocus) {
this.props.onFocus(ev);
}
};
_onBlur = ev => {
private onBlur = (ev: React.FocusEvent): void => {
this.setState({ blurred: true });
if (this.props.onBlur) {
this.props.onBlur(ev);
}
};
_clearSearch(source) {
this._search.current.value = "";
private clearSearch(source?: string): void {
this.search.current.value = "";
this.onChange();
if (this.props.onCleared) {
this.props.onCleared(source);
}
}
render() {
public render(): JSX.Element {
// check for collapsed here and
// not at parent so we keep
// searchTerm in our state
@ -136,7 +145,7 @@ export default class SearchBox extends React.Component {
key="button"
tabIndex={-1}
className="mx_SearchBox_closeButton"
onClick={() => {this._clearSearch("button"); }}
onClick={() => {this.clearSearch("button"); }}
/>) : undefined;
// show a shorter placeholder when blurred, if requested
@ -151,13 +160,13 @@ export default class SearchBox extends React.Component {
<input
key="searchfield"
type="text"
ref={this._search}
ref={this.search}
className={"mx_textinput_icon mx_textinput_search " + className}
value={this.state.searchTerm}
onFocus={this._onFocus}
onFocus={this.onFocus}
onChange={this.onChange}
onKeyDown={this._onKeyDown}
onBlur={this._onBlur}
onKeyDown={this.onKeyDown}
onBlur={this.onBlur}
placeholder={placeholder}
autoComplete="off"
autoFocus={this.props.autoFocus}

View file

@ -16,52 +16,60 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
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")
export default class UserView extends React.Component {
static get propTypes() {
return {
userId: PropTypes.string,
export default class UserView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
loading: true,
};
}
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
public componentDidMount(): void {
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
// 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();
this.loadProfileInfo();
}
}
async _loadProfileInfo() {
private async loadProfileInfo(): Promise<void> {
const cli = MatrixClientPeg.get();
this.setState({ loading: true });
let profileInfo;
try {
profileInfo = await cli.getProfileInfo(this.props.userId);
} catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog(_t('Could not load user profile'), '', ErrorDialog, {
title: _t('Could not load user profile'),
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 });
}
render() {
public render(): JSX.Element {
if (this.state.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
} else if (this.state.member) {
const RightPanel = sdk.getComponent('structures.RightPanel');
const MainSplit = sdk.getComponent('structures.MainSplit');
const panel = <RightPanel user={this.state.member} />;
} else if (this.state.member?.user) {
const panel = <RightPanel user={this.state.member.user} resizeNotifier={this.props.resizeNotifier} />;
return (<MainSplit panel={panel} resizeNotifier={this.props.resizeNotifier}>
<HomePage />
</MainSplit>);

View file

@ -17,24 +17,28 @@ limitations under the License.
*/
import React from "react";
import PropTypes from "prop-types";
import SyntaxHighlight from "../views/elements/SyntaxHighlight";
import { _t } from "../../languageHandler";
import * as sdk from "../../index";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog";
import { canEditContent } from "../../utils/EventUtils";
import { MatrixClientPeg } from '../../MatrixClientPeg';
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")
export default class ViewSource extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu
};
constructor(props) {
export default class ViewSource extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
@ -42,19 +46,20 @@ export default class ViewSource extends React.Component {
};
}
onBack() {
private onBack(): void {
// TODO: refresh the "Event ID:" modal header
this.setState({ isEditing: false });
}
onEdit() {
private onEdit(): void {
this.setState({ isEditing: true });
}
// 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 isEncrypted = mxEvent.isEncrypted();
// @ts-ignore
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
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
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 isEncrypted = mxEvent.isEncrypted();
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
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 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 room = cli.getRoom(mxEvent.getRoomId());
return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli);
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
public render(): JSX.Element {
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;

View file

@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content">

View file

@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">

View file

@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">

View file

@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search spaces")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">