Merge pull request #6815 from SimonBrandner/task/elements-ts

Convert `/src/components/views/elements` to TS
This commit is contained in:
Travis Ralston 2021-09-21 09:12:56 -06:00 committed by GitHub
commit 2eea606442
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 648 additions and 613 deletions

View file

@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
import Field from '../../../../components/views/elements/Field'; import Field from '../../../../components/views/elements/Field';
import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import DialogButtons from "../../../../components/views/elements/DialogButtons"; import DialogButtons from "../../../../components/views/elements/DialogButtons";
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (confirmed: boolean) => void;
}
interface IState { interface IState {
eventIndexSize: number; eventIndexSize: number;

View file

@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
<AppTile <AppTile
app={app} app={app}
fullWidth fullWidth
show
showMenubar={false} showMenubar={false}
userWidget userWidget
userId={cli.getUserId()} userId={cli.getUserId()}

View file

@ -23,10 +23,9 @@ import Modal from '../../../Modal';
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import QuestionDialog from "./QuestionDialog"; import QuestionDialog from "./QuestionDialog";
import { IDialogProps } from "./IDialogProps";
interface IProps { interface IProps extends IDialogProps {}
onFinished: (success: boolean) => void;
}
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => { const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;

View file

@ -19,7 +19,6 @@ limitations under the License.
import url from 'url'; import url from 'url';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -39,33 +38,95 @@ import { MatrixCapabilities } from "matrix-widget-api";
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu"; import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
import WidgetAvatar from "../avatars/WidgetAvatar"; import WidgetAvatar from "../avatars/WidgetAvatar";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Room } from "matrix-js-sdk/src/models/room";
import { IApp } from "../../../stores/WidgetStore";
interface IProps {
app: IApp;
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: Room;
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth?: boolean;
// Optional. If set, renders a smaller view of the widget
miniMode?: boolean;
// UserId of the current user
userId: string;
// UserId of the entity that added / modified the widget
creatorUserId: string;
waitForIframeLoad: boolean;
showMenubar?: boolean;
// Optional onEditClickHandler (overrides default behaviour)
onEditClick?: () => void;
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick?: () => void;
// Optionally hide the tile title
showTitle?: boolean;
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents?: boolean;
// Optionally hide the popout widget icon
showPopout?: boolean;
// Is this an instance of a user widget
userWidget: boolean;
// sets the pointer-events property on the iframe
pointerEvents?: string;
widgetPageTitle?: string;
}
interface IState {
initialising: boolean; // True while we are mangling the widget URL
// True while the iframe content is loading
loading: boolean;
// Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user
hasPermissionToLoad: boolean;
error: Error;
menuDisplayed: boolean;
widgetPageTitle: string;
}
@replaceableComponent("views.elements.AppTile") @replaceableComponent("views.elements.AppTile")
export default class AppTile extends React.Component { export default class AppTile extends React.Component<IProps, IState> {
constructor(props) { public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};
private contextMenuButton = createRef<any>();
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
private allowedWidgetsWatchRef: string;
private persistKey: string;
private sgWidget: StopGapWidget;
private dispatcherRef: string;
constructor(props: IProps) {
super(props); super(props);
// The key used for PersistedElement // The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id); this.persistKey = getPersistKey(this.props.app.id);
try { try {
this._sgWidget = new StopGapWidget(this.props); this.sgWidget = new StopGapWidget(this.props);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); console.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props); this.state = this.getNewState(props);
this._contextMenuButton = createRef();
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
} }
// This is a function to make the impact of calling SettingsStore slightly less // This is a function to make the impact of calling SettingsStore slightly less
hasPermissionToLoad = (props) => { private hasPermissionToLoad = (props: IProps): boolean => {
if (this._usingLocalWidget()) return true; if (this.usingLocalWidget()) return true;
if (!props.room) return true; // user widgets always have permissions if (!props.room) return true; // user widgets always have permissions
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
@ -81,34 +142,34 @@ export default class AppTile extends React.Component {
* @param {Object} newProps The new properties of the component * @param {Object} newProps The new properties of the component
* @return {Object} Updated component state to be set with setState * @return {Object} Updated component state to be set with setState
*/ */
_getNewState(newProps) { private getNewState(newProps: IProps): IState {
return { return {
initialising: true, // True while we are mangling the widget URL initialising: true, // True while we are mangling the widget URL
// True while the iframe content is loading // True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
// Assume that widget has permission to load if we are the user who // Assume that widget has permission to load if we are the user who
// added it to the room, or if explicitly granted by the user // added it to the room, or if explicitly granted by the user
hasPermissionToLoad: this.hasPermissionToLoad(newProps), hasPermissionToLoad: this.hasPermissionToLoad(newProps),
error: null, error: null,
widgetPageTitle: newProps.widgetPageTitle,
menuDisplayed: false, menuDisplayed: false,
widgetPageTitle: this.props.widgetPageTitle,
}; };
} }
onAllowedWidgetsChange = () => { private onAllowedWidgetsChange = (): void => {
const hasPermissionToLoad = this.hasPermissionToLoad(this.props); const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
if (this._sgWidget) this._sgWidget.stop(); if (this.sgWidget) this.sgWidget.stop();
} }
this.setState({ hasPermissionToLoad }); this.setState({ hasPermissionToLoad });
}; };
isMixedContent() { private isMixedContent(): boolean {
const parentContentProtocol = window.location.protocol; const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.app.url); const u = url.parse(this.props.app.url);
const childContentProtocol = u.protocol; const childContentProtocol = u.protocol;
@ -120,69 +181,70 @@ export default class AppTile extends React.Component {
return false; return false;
} }
componentDidMount() { public componentDidMount(): void {
// Only fetch IM token on mount if we're showing and have permission to load // Only fetch IM token on mount if we're showing and have permission to load
if (this._sgWidget && this.state.hasPermissionToLoad) { if (this.sgWidget && this.state.hasPermissionToLoad) {
this._startWidget(); this.startWidget();
} }
// Widget action listeners // Widget action listeners
this.dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { public componentWillUnmount(): void {
// Widget action listeners // Widget action listeners
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container // if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
} }
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
} }
_resetWidget(newProps) { private resetWidget(newProps: IProps): void {
if (this._sgWidget) { if (this.sgWidget) {
this._sgWidget.stop(); this.sgWidget.stop();
} }
try { try {
this._sgWidget = new StopGapWidget(newProps); this.sgWidget = new StopGapWidget(newProps);
this._sgWidget.on("preparing", this._onWidgetPrepared); this.sgWidget.on("preparing", this.onWidgetPrepared);
this._sgWidget.on("ready", this._onWidgetReady); this.sgWidget.on("ready", this.onWidgetReady);
this._startWidget(); this.startWidget();
} catch (e) { } catch (e) {
console.log("Failed to construct widget", e); console.log("Failed to construct widget", e);
this._sgWidget = null; this.sgWidget = null;
} }
} }
_startWidget() { private startWidget(): void {
this._sgWidget.prepare().then(() => { this.sgWidget.prepare().then(() => {
this.setState({ initialising: false }); this.setState({ initialising: false });
}); });
} }
_iframeRefChange = (ref) => { private iframeRefChange = (ref: HTMLIFrameElement): void => {
this.iframe = ref; this.iframe = ref;
if (ref) { if (ref) {
if (this._sgWidget) this._sgWidget.start(ref); if (this.sgWidget) this.sgWidget.start(ref);
} else { } else {
this._resetWidget(this.props); this.resetWidget(this.props);
} }
}; };
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
if (nextProps.app.url !== this.props.app.url) { if (nextProps.app.url !== this.props.app.url) {
this._getNewState(nextProps); this.getNewState(nextProps);
if (this.state.hasPermissionToLoad) { if (this.state.hasPermissionToLoad) {
this._resetWidget(nextProps); this.resetWidget(nextProps);
} }
} }
@ -198,7 +260,7 @@ export default class AppTile extends React.Component {
* @private * @private
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed. * @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
*/ */
async _endWidgetActions() { // widget migration dev note: async to maintain signature private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
// HACK: This is a really dirty way to ensure that Jitsi cleans up // HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media // its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351 // stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
@ -217,27 +279,27 @@ export default class AppTile extends React.Component {
} }
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey); PersistedElement.destroyElement(this.persistKey);
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }
_onWidgetPrepared = () => { private onWidgetPrepared = (): void => {
this.setState({ loading: false }); this.setState({ loading: false });
}; };
_onWidgetReady = () => { private onWidgetReady = (): void => {
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {}); this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
} }
}; };
_onAction = payload => { private onAction = (payload): void => {
if (payload.widgetId === this.props.app.id) { if (payload.widgetId === this.props.app.id) {
switch (payload.action) { switch (payload.action) {
case 'm.sticker': case 'm.sticker':
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) { if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
dis.dispatch({ action: 'post_sticker_message', data: payload.data }); dis.dispatch({ action: 'post_sticker_message', data: payload.data });
dis.dispatch({ action: 'stickerpicker_close' }); dis.dispatch({ action: 'stickerpicker_close' });
} else { } else {
@ -248,7 +310,7 @@ export default class AppTile extends React.Component {
} }
}; };
_grantWidgetPermission = () => { private grantWidgetPermission = (): void => {
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
console.info("Granting permission for widget to load: " + this.props.app.eventId); console.info("Granting permission for widget to load: " + this.props.app.eventId);
const current = SettingsStore.getValue("allowedWidgets", roomId); const current = SettingsStore.getValue("allowedWidgets", roomId);
@ -258,14 +320,14 @@ export default class AppTile extends React.Component {
this.setState({ hasPermissionToLoad: true }); this.setState({ hasPermissionToLoad: true });
// Fetch a token for the integration manager, now that we're allowed to // Fetch a token for the integration manager, now that we're allowed to
this._startWidget(); this.startWidget();
}).catch(err => { }).catch(err => {
console.error(err); console.error(err);
// We don't really need to do anything about this - the user will just hit the button again. // We don't really need to do anything about this - the user will just hit the button again.
}); });
}; };
formatAppTileName() { private formatAppTileName(): string {
let appTileName = "No name"; let appTileName = "No name";
if (this.props.app.name && this.props.app.name.trim()) { if (this.props.app.name && this.props.app.name.trim()) {
appTileName = this.props.app.name.trim(); appTileName = this.props.app.name.trim();
@ -278,11 +340,11 @@ export default class AppTile extends React.Component {
* actual widget URL * actual widget URL
* @returns {bool} true If using a local version of the widget * @returns {bool} true If using a local version of the widget
*/ */
_usingLocalWidget() { private usingLocalWidget(): boolean {
return WidgetType.JITSI.matches(this.props.app.type); return WidgetType.JITSI.matches(this.props.app.type);
} }
_getTileTitle() { private getTileTitle(): JSX.Element {
const name = this.formatAppTileName(); const name = this.formatAppTileName();
const titleSpacer = <span>&nbsp;-&nbsp;</span>; const titleSpacer = <span>&nbsp;-&nbsp;</span>;
let title = ''; let title = '';
@ -300,32 +362,32 @@ export default class AppTile extends React.Component {
} }
// TODO replace with full screen interactions // TODO replace with full screen interactions
_onPopoutWidgetClick = () => { private onPopoutWidgetClick = (): void => {
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
if (WidgetType.JITSI.matches(this.props.app.type)) { if (WidgetType.JITSI.matches(this.props.app.type)) {
this._endWidgetActions().then(() => { this.endWidgetActions().then(() => {
if (this.iframe) { if (this.iframe) {
// Reload iframe // Reload iframe
this.iframe.src = this._sgWidget.embedUrl; this.iframe.src = this.sgWidget.embedUrl;
} }
}); });
} }
// Using Object.assign workaround as the following opens in a new window instead of a new tab. // Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'), Object.assign(document.createElement('a'),
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click(); { target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
}; };
_onContextMenuClick = () => { private onContextMenuClick = (): void => {
this.setState({ menuDisplayed: true }); this.setState({ menuDisplayed: true });
}; };
_closeContextMenu = () => { private closeContextMenu = (): void => {
this.setState({ menuDisplayed: false }); this.setState({ menuDisplayed: false });
}; };
render() { public render(): JSX.Element {
let appTileBody; let appTileBody;
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
@ -351,7 +413,7 @@ export default class AppTile extends React.Component {
<Spinner message={_t("Loading...")} /> <Spinner message={_t("Loading...")} />
</div> </div>
); );
if (this._sgWidget === null) { if (this.sgWidget === null) {
appTileBody = ( appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}> <div className={appTileBodyClass} style={appTileBodyStyles}>
<AppWarning errorMsg={_t("Error loading Widget")} /> <AppWarning errorMsg={_t("Error loading Widget")} />
@ -365,9 +427,9 @@ export default class AppTile extends React.Component {
<AppPermission <AppPermission
roomId={this.props.room.roomId} roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId} creatorUserId={this.props.creatorUserId}
url={this._sgWidget.embedUrl} url={this.sgWidget.embedUrl}
isRoomEncrypted={isEncrypted} isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission} onPermissionGranted={this.grantWidgetPermission}
/> />
</div> </div>
); );
@ -390,8 +452,8 @@ export default class AppTile extends React.Component {
{ this.state.loading && loadingElement } { this.state.loading && loadingElement }
<iframe <iframe
allow={iframeFeatures} allow={iframeFeatures}
ref={this._iframeRefChange} ref={this.iframeRefChange}
src={this._sgWidget.embedUrl} src={this.sgWidget.embedUrl}
allowFullScreen={true} allowFullScreen={true}
sandbox={sandboxFlags} sandbox={sandboxFlags}
/> />
@ -407,7 +469,7 @@ export default class AppTile extends React.Component {
// Also wrap the PersistedElement in a div to fix the height, otherwise // Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place // AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper"> appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}> <PersistedElement persistKey={this.persistKey}>
{ appTileBody } { appTileBody }
</PersistedElement> </PersistedElement>
</div>; </div>;
@ -429,9 +491,9 @@ export default class AppTile extends React.Component {
if (this.state.menuDisplayed) { if (this.state.menuDisplayed) {
contextMenu = ( contextMenu = (
<RoomWidgetContextMenu <RoomWidgetContextMenu
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)} {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
app={this.props.app} app={this.props.app}
onFinished={this._closeContextMenu} onFinished={this.closeContextMenu}
showUnpin={!this.props.userWidget} showUnpin={!this.props.userWidget}
userWidget={this.props.userWidget} userWidget={this.props.userWidget}
onEditClick={this.props.onEditClick} onEditClick={this.props.onEditClick}
@ -444,21 +506,21 @@ export default class AppTile extends React.Component {
<div className={appTileClasses} id={this.props.app.id}> <div className={appTileClasses} id={this.props.app.id}>
{ this.props.showMenubar && { this.props.showMenubar &&
<div className="mx_AppTileMenuBar"> <div className="mx_AppTileMenuBar">
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}> <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
{ this.props.showTitle && this._getTileTitle() } { this.props.showTitle && this.getTileTitle() }
</span> </span>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ this.props.showPopout && <AccessibleButton { this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')} title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick} onClick={this.onPopoutWidgetClick}
/> } /> }
<ContextMenuButton <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu" className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
label={_t("Options")} label={_t("Options")}
isExpanded={this.state.menuDisplayed} isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton} inputRef={this.contextMenuButton}
onClick={this._onContextMenuClick} onClick={this.onContextMenuClick}
/> />
</span> </span>
</div> } </div> }
@ -469,49 +531,3 @@ export default class AppTile extends React.Component {
</React.Fragment>; </React.Fragment>;
} }
} }
AppTile.displayName = 'AppTile';
AppTile.propTypes = {
app: PropTypes.object.isRequired,
// If room is not specified then it is an account level widget
// which bypasses permission prompts as it was added explicitly by that user
room: PropTypes.object,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth: PropTypes.bool,
// Optional. If set, renders a smaller view of the widget
miniMode: PropTypes.bool,
// UserId of the current user
userId: PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget
creatorUserId: PropTypes.string,
waitForIframeLoad: PropTypes.bool,
showMenubar: PropTypes.bool,
// Optional onEditClickHandler (overrides default behaviour)
onEditClick: PropTypes.func,
// Optional onDeleteClickHandler (overrides default behaviour)
onDeleteClick: PropTypes.func,
// Optional onMinimiseClickHandler
onMinimiseClick: PropTypes.func,
// Optionally hide the tile title
showTitle: PropTypes.bool,
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
// sets the pointer-events property on the iframe
pointerEvents: PropTypes.string,
};
AppTile.defaultProps = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showPopout: true,
handleMinimisePointerEvents: false,
userWidget: false,
miniMode: false,
};

View file

@ -1,24 +1,20 @@
import React from 'react'; // eslint-disable-line no-unused-vars import React from 'react';
import PropTypes from 'prop-types';
const AppWarning = (props) => { interface IProps {
errorMsg?: string;
}
const AppWarning: React.FC<IProps> = (props) => {
return ( return (
<div className='mx_AppPermissionWarning'> <div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'> <div className='mx_AppPermissionWarningImage'>
<img src={require("../../../../res/img/warning.svg")} alt='' /> <img src={require("../../../../res/img/warning.svg")} alt='' />
</div> </div>
<div className='mx_AppPermissionWarningText'> <div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span> <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
</div> </div>
</div> </div>
); );
}; };
AppWarning.propTypes = {
errorMsg: PropTypes.string,
};
AppWarning.defaultProps = {
errorMsg: 'Error',
};
export default AppWarning; export default AppWarning;

View file

@ -17,60 +17,61 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from "prop-types";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// The primary button which is styled differently and has default focus.
primaryButton: React.ReactNode;
// A node to insert into the cancel button instead of default "Cancel"
cancelButton?: React.ReactNode;
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit?: boolean;
// onClick handler for the primary button.
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
// should there be a cancel button? default: true
hasCancel?: boolean;
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass?: string;
// onClick handler for the cancel button.
onCancel?: (...args: any[]) => void;
focus?: boolean;
// disables the primary and cancel buttons
disabled?: boolean;
// disables only the primary button
primaryDisabled?: boolean;
// something to stick next to the buttons, optionally
additive?: React.ReactNode;
primaryButtonClass?: string;
}
/** /**
* Basic container for buttons in modal dialogs. * Basic container for buttons in modal dialogs.
*/ */
@replaceableComponent("views.elements.DialogButtons") @replaceableComponent("views.elements.DialogButtons")
export default class DialogButtons extends React.Component { export default class DialogButtons extends React.Component<IProps> {
static propTypes = { public static defaultProps: Partial<IProps> = {
// The primary button which is styled differently and has default focus.
primaryButton: PropTypes.node.isRequired,
// A node to insert into the cancel button instead of default "Cancel"
cancelButton: PropTypes.node,
// If true, make the primary button a form submit button (input type="submit")
primaryIsSubmit: PropTypes.bool,
// onClick handler for the primary button.
onPrimaryButtonClick: PropTypes.func,
// should there be a cancel button? default: true
hasCancel: PropTypes.bool,
// The class of the cancel button, only used if a cancel button is
// enabled
cancelButtonClass: PropTypes.node,
// onClick handler for the cancel button.
onCancel: PropTypes.func,
focus: PropTypes.bool,
// disables the primary and cancel buttons
disabled: PropTypes.bool,
// disables only the primary button
primaryDisabled: PropTypes.bool,
// something to stick next to the buttons, optionally
additive: PropTypes.element,
};
static defaultProps = {
hasCancel: true, hasCancel: true,
disabled: false, disabled: false,
}; };
_onCancelClick = () => { private onCancelClick = (event: React.MouseEvent): void => {
this.props.onCancel(); this.props.onCancel(event);
}; };
render() { public render(): JSX.Element {
let primaryButtonClassName = "mx_Dialog_primary"; let primaryButtonClassName = "mx_Dialog_primary";
if (this.props.primaryButtonClass) { if (this.props.primaryButtonClass) {
primaryButtonClassName += " " + this.props.primaryButtonClass; primaryButtonClassName += " " + this.props.primaryButtonClass;
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
// important: the default type is 'submit' and this button comes before the // important: the default type is 'submit' and this button comes before the
// primary in the DOM so will get form submissions unless we make it not a submit. // primary in the DOM so will get form submissions unless we make it not a submit.
type="button" type="button"
onClick={this._onCancelClick} onClick={this.onCancelClick}
className={this.props.cancelButtonClass} className={this.props.cancelButtonClass}
disabled={this.props.disabled} disabled={this.props.disabled}
> >

View file

@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ChangeEvent, createRef } from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleButton from "./AccessibleButton";
interface IProps {
className?: string;
onChange?: (value: string) => void;
onClear?: () => void;
onJoinClick?: (value: string) => void;
placeholder?: string;
showJoinButton?: boolean;
initialText?: string;
}
interface IState {
value: string;
}
@replaceableComponent("views.elements.DirectorySearchBox") @replaceableComponent("views.elements.DirectorySearchBox")
export default class DirectorySearchBox extends React.Component { export default class DirectorySearchBox extends React.Component<IProps, IState> {
constructor(props) { private input = createRef<HTMLInputElement>();
super(props);
this._collectInput = this._collectInput.bind(this);
this._onClearClick = this._onClearClick.bind(this);
this._onChange = this._onChange.bind(this);
this._onKeyUp = this._onKeyUp.bind(this);
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
this.input = null; constructor(props: IProps) {
super(props);
this.state = { this.state = {
value: this.props.initialText || '', value: this.props.initialText || '',
}; };
} }
_collectInput(e) { private onClearClick = (): void => {
this.input = e;
}
_onClearClick() {
this.setState({ value: '' }); this.setState({ value: '' });
if (this.input) { if (this.input.current) {
this.input.focus(); this.input.current.focus();
if (this.props.onClear) { if (this.props.onClear) {
this.props.onClear(); this.props.onClear();
} }
} }
} };
_onChange(ev) { private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
if (!this.input) return; if (!this.input.current) return;
this.setState({ value: ev.target.value }); this.setState({ value: ev.target.value });
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(ev.target.value); this.props.onChange(ev.target.value);
} }
} };
_onKeyUp(ev) { private onKeyUp = (ev: React.KeyboardEvent): void => {
if (ev.key == 'Enter' && this.props.showJoinButton) { if (ev.key == 'Enter' && this.props.showJoinButton) {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} }
} };
_onJoinButtonClick() { private onJoinButtonClick = (): void => {
if (this.props.onJoinClick) { if (this.props.onJoinClick) {
this.props.onJoinClick(this.state.value); this.props.onJoinClick(this.state.value);
} }
} };
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
public render(): JSX.Element {
const searchboxClasses = { const searchboxClasses = {
mx_DirectorySearchBox: true, mx_DirectorySearchBox: true,
}; };
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
let joinButton; let joinButton;
if (this.props.showJoinButton) { if (this.props.showJoinButton) {
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton" joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
onClick={this._onJoinButtonClick} onClick={this.onJoinButtonClick}
>{ _t("Join") }</AccessibleButton>; >{ _t("Join") }</AccessibleButton>;
} }
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
name="dirsearch" name="dirsearch"
value={this.state.value} value={this.state.value}
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
ref={this._collectInput} ref={this.input}
onChange={this._onChange} onChange={this.onChange}
onKeyUp={this._onKeyUp} onKeyUp={this.onKeyUp}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
autoFocus autoFocus
/> />
{ joinButton } { joinButton }
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
</div>; </div>;
} }
} }
DirectorySearchBox.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
onJoinClick: PropTypes.func,
placeholder: PropTypes.string,
showJoinButton: PropTypes.bool,
initialText: PropTypes.string,
};

View file

@ -16,33 +16,42 @@ 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 { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.EditableText") enum Phases {
export default class EditableText extends React.Component { Display = "display",
static propTypes = { Edit = "edit",
onValueChanged: PropTypes.func, }
initialValue: PropTypes.string,
label: PropTypes.string, interface IProps {
placeholder: PropTypes.string, onValueChanged?: (value: string, shouldSubmit: boolean) => void;
className: PropTypes.string, initialValue?: string;
labelClassName: PropTypes.string, label?: string;
placeholderClassName: PropTypes.string, placeholder?: string;
className?: string;
labelClassName?: string;
placeholderClassName?: string;
// Overrides blurToSubmit if true // Overrides blurToSubmit if true
blurToCancel: PropTypes.bool, blurToCancel?: boolean;
// Will cause onValueChanged(value, true) to fire on blur // Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: PropTypes.bool, blurToSubmit?: boolean;
editable: PropTypes.bool, editable?: boolean;
}; }
static Phases = { interface IState {
Display: "display", phase: Phases;
Edit: "edit", }
};
static defaultProps = { @replaceableComponent("views.elements.EditableText")
export default class EditableText extends React.Component<IProps, IState> {
// we track value as an JS object field rather than in React state
// as React doesn't play nice with contentEditable.
public value = '';
private placeholder = false;
private editableDiv = createRef<HTMLDivElement>();
public static defaultProps: Partial<IProps> = {
onValueChanged() {}, onValueChanged() {},
initialValue: '', initialValue: '',
label: '', label: '',
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
blurToSubmit: false, blurToSubmit: false,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
// we track value as an JS object field rather than in React state this.state = {
// as React doesn't play nice with contentEditable. phase: Phases.Display,
this.value = '';
this.placeholder = false;
this._editable_div = createRef();
}
state = {
phase: EditableText.Phases.Display,
}; };
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
UNSAFE_componentWillReceiveProps(nextProps) { public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
if (nextProps.initialValue !== this.props.initialValue) { if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue; this.value = nextProps.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
} }
componentDidMount() { public componentDidMount(): void {
this.value = this.props.initialValue; this.value = this.props.initialValue;
if (this._editable_div.current) { if (this.editableDiv.current) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
} }
} }
showPlaceholder = show => { private showPlaceholder = (show: boolean): void => {
if (show) { if (show) {
this._editable_div.current.textContent = this.props.placeholder; this.editableDiv.current.textContent = this.props.placeholder;
this._editable_div.current.setAttribute("class", this.props.className this.editableDiv.current.setAttribute("class", this.props.className
+ " " + this.props.placeholderClassName); + " " + this.props.placeholderClassName);
this.placeholder = true; this.placeholder = true;
this.value = ''; this.value = '';
} else { } else {
this._editable_div.current.textContent = this.value; this.editableDiv.current.textContent = this.value;
this._editable_div.current.setAttribute("class", this.props.className); this.editableDiv.current.setAttribute("class", this.props.className);
this.placeholder = false; this.placeholder = false;
} }
}; };
getValue = () => this.value; private cancelEdit = (): void => {
setValue = value => {
this.value = value;
this.showPlaceholder(!this.value);
};
edit = () => {
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Display,
});
};
cancelEdit = () => {
this.setState({
phase: EditableText.Phases.Display,
}); });
this.value = this.props.initialValue; this.value = this.props.initialValue;
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
this.onValueChanged(false); this.onValueChanged(false);
this._editable_div.current.blur(); this.editableDiv.current.blur();
}; };
onValueChanged = shouldSubmit => { private onValueChanged = (shouldSubmit: boolean): void => {
this.props.onValueChanged(this.value, shouldSubmit); this.props.onValueChanged(this.value, shouldSubmit);
}; };
onKeyDown = ev => { private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (this.placeholder) { if (this.placeholder) {
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onKeyUp = ev => { private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
if (!ev.target.textContent) { if (!(ev.target as HTMLDivElement).textContent) {
this.showPlaceholder(true); this.showPlaceholder(true);
} else if (!this.placeholder) { } else if (!this.placeholder) {
this.value = ev.target.textContent; this.value = (ev.target as HTMLDivElement).textContent;
} }
if (ev.key === Key.ENTER) { if (ev.key === Key.ENTER) {
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder); // console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
}; };
onClickDiv = ev => { private onClickDiv = (): void => {
if (!this.props.editable) return; if (!this.props.editable) return;
this.setState({ this.setState({
phase: EditableText.Phases.Edit, phase: Phases.Edit,
}); });
}; };
onFocus = ev => { private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
//ev.target.setSelectionRange(0, ev.target.textContent.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
const node = ev.target.childNodes[0]; const node = ev.target.childNodes[0];
if (node) { if (node) {
const range = document.createRange(); const range = document.createRange();
range.setStart(node, 0); range.setStart(node, 0);
range.setEnd(node, node.length); range.setEnd(node, ev.target.childNodes.length);
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
} }
}; };
onFinish = (ev, shouldSubmit) => { private onFinish = (
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
shouldSubmit?: boolean,
): void => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this; const self = this;
const submit = (ev.key === Key.ENTER) || shouldSubmit; const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
this.setState({ this.setState({
phase: EditableText.Phases.Display, phase: Phases.Display,
}, () => { }, () => {
if (this.value !== this.props.initialValue) { if (this.value !== this.props.initialValue) {
self.onValueChanged(submit); self.onValueChanged(submit);
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
}); });
}; };
onBlur = ev => { private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
const sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
}; };
render() { public render(): JSX.Element {
const { className, editable, initialValue, label, labelClassName } = this.props; const { className, editable, initialValue, label, labelClassName } = this.props;
let editableEl; let editableEl;
if (!editable || (this.state.phase === EditableText.Phases.Display && if (!editable || (this.state.phase === Phases.Display &&
(label || labelClassName) && !this.value) (label || labelClassName) && !this.value)
) { ) {
// show the label // show the label
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
} else { } else {
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
editableEl = <div editableEl = <div
ref={this._editable_div} ref={this.editableDiv}
contentEditable={true} contentEditable={true}
className={className} className={className}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View file

@ -15,9 +15,34 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import EditableText from "./EditableText";
interface IProps {
/* callback to retrieve the initial value. */
getInitialValue?: () => Promise<string>;
/* initial value; used if getInitialValue is not given */
initialValue?: string;
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder?: string;
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit?: (value: string) => Promise<{} | void>;
/* should the input submit when focus is lost? */
blurToSubmit?: boolean;
}
interface IState {
busy: boolean;
errorString: string;
value: string;
}
/** /**
* A component which wraps an EditableText, with a spinner while updates take * A component which wraps an EditableText, with a spinner while updates take
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
* taken from the 'initialValue' property. * taken from the 'initialValue' property.
*/ */
@replaceableComponent("views.elements.EditableTextContainer") @replaceableComponent("views.elements.EditableTextContainer")
export default class EditableTextContainer extends React.Component { export default class EditableTextContainer extends React.Component<IProps, IState> {
constructor(props) { private unmounted = false;
public static defaultProps: Partial<IProps> = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: () => { return Promise.resolve(); },
};
constructor(props: IProps) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
busy: false, busy: false,
errorString: null, errorString: null,
value: props.initialValue, value: props.initialValue,
}; };
this._onValueChanged = this._onValueChanged.bind(this);
} }
componentDidMount() { public async componentDidMount(): Promise<void> {
if (this.props.getInitialValue === undefined) {
// use whatever was given in the initialValue property. // use whatever was given in the initialValue property.
return; if (this.props.getInitialValue === undefined) return;
}
this.setState({ busy: true }); this.setState({ busy: true });
try {
this.props.getInitialValue().then( const initialValue = await this.props.getInitialValue();
(result) => { if (this.unmounted) return;
if (this._unmounted) { return; }
this.setState({ this.setState({
busy: false, busy: false,
value: result, value: initialValue,
}); });
}, } catch (error) {
(error) => { if (this.unmounted) return;
if (this._unmounted) { return; }
this.setState({ this.setState({
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
}, }
);
} }
componentWillUnmount() { public componentWillUnmount(): void {
this._unmounted = true; this.unmounted = true;
} }
_onValueChanged(value, shouldSubmit) { private onValueChanged = (value: string, shouldSubmit: boolean): void => {
if (!shouldSubmit) { if (!shouldSubmit) {
return; return;
} }
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
this.props.onSubmit(value).then( this.props.onSubmit(value).then(
() => { () => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
busy: false, busy: false,
value: value, value: value,
}); });
}, },
(error) => { (error) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ this.setState({
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
}, },
); );
} };
render() { public render(): JSX.Element {
if (this.state.busy) { if (this.state.busy) {
const Loader = sdk.getComponent("elements.Spinner");
return ( return (
<Loader /> <Spinner />
); );
} else if (this.state.errorString) { } else if (this.state.errorString) {
return ( return (
<div className="error">{ this.state.errorString }</div> <div className="error">{ this.state.errorString }</div>
); );
} else { } else {
const EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText initialValue={this.state.value} <EditableText initialValue={this.state.value}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onValueChanged={this._onValueChanged} onValueChanged={this.onValueChanged}
blurToSubmit={this.props.blurToSubmit} blurToSubmit={this.props.blurToSubmit}
/> />
); );
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
} }
} }
EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */
getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */
initialValue: PropTypes.string,
/* placeholder text to use when the value is empty (and not being
* edited) */
placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new
* value. */
onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */
blurToSubmit: PropTypes.bool,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",
blurToSubmit: false,
onSubmit: function(v) {return Promise.resolve(); },
};

View file

@ -16,13 +16,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from "./Spinner";
import Dropdown from "./Dropdown";
function languageMatchesSearchQuery(query, language) { function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().includes(query.toUpperCase())) return true; if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
return false; return false;
} }
interface IProps {
className?: string;
onOptionChange: (language: string) => void;
value?: string;
disabled?: boolean;
}
interface IState {
searchQuery: string;
langs: string[];
}
@replaceableComponent("views.elements.LanguageDropdown") @replaceableComponent("views.elements.LanguageDropdown")
export default class LanguageDropdown extends React.Component { export default class LanguageDropdown extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._onSearchChange = this._onSearchChange.bind(this);
this.state = { this.state = {
searchQuery: '', searchQuery: '',
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
}; };
} }
componentDidMount() { public componentDidMount(): void {
languageHandler.getAllLanguagesFromJson().then((langs) => { languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b) { langs.sort(function(a, b) {
if (a.label < b.label) return -1; if (a.label < b.label) return -1;
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
} }
} }
_onSearchChange(search) { private onSearchChange = (search: string): void => {
this.setState({ this.setState({
searchQuery: search, searchQuery: search,
}); });
} };
render() { public render(): JSX.Element {
if (this.state.langs === null) { if (this.state.langs === null) {
const Spinner = sdk.getComponent('elements.Spinner');
return <Spinner />; return <Spinner />;
} }
const Dropdown = sdk.getComponent('elements.Dropdown');
let displayedLanguages; let displayedLanguages;
if (this.state.searchQuery) { if (this.state.searchQuery) {
displayedLanguages = this.state.langs.filter((lang) => { displayedLanguages = this.state.langs.filter((lang) => {
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
id="mx_LanguageDropdown" id="mx_LanguageDropdown"
className={this.props.className} className={this.props.className}
onOptionChange={this.props.onOptionChange} onOptionChange={this.props.onOptionChange}
onSearchChange={this._onSearchChange} onSearchChange={this.onSearchChange}
searchEnabled={true} searchEnabled={true}
value={value} value={value}
label={_t("Language Dropdown")} label={_t("Language Dropdown")}
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
} }
} }
LanguageDropdown.propTypes = {
className: PropTypes.string,
onOptionChange: PropTypes.func.isRequired,
value: PropTypes.string,
};

View file

@ -15,17 +15,16 @@ 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";
class ItemRange { class ItemRange {
constructor(topCount, renderCount, bottomCount) { constructor(
this.topCount = topCount; public topCount: number,
this.renderCount = renderCount; public renderCount: number,
this.bottomCount = bottomCount; public bottomCount: number,
} ) { }
contains(range) { public contains(range: ItemRange): boolean {
// don't contain empty ranges // don't contain empty ranges
// as it will prevent clearing the list // as it will prevent clearing the list
// once it is scrolled far enough out of view // once it is scrolled far enough out of view
@ -36,7 +35,7 @@ class ItemRange {
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount); (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
} }
expand(amount) { public expand(amount: number): ItemRange {
// don't expand ranges that won't render anything // don't expand ranges that won't render anything
if (this.renderCount === 0) { if (this.renderCount === 0) {
return this; return this;
@ -51,20 +50,55 @@ class ItemRange {
); );
} }
totalSize() { public totalSize(): number {
return this.topCount + this.renderCount + this.bottomCount; return this.topCount + this.renderCount + this.bottomCount;
} }
} }
@replaceableComponent("views.elements.LazyRenderList") interface IProps<T> {
export default class LazyRenderList extends React.Component { // height in pixels of the component returned by `renderItem`
constructor(props) { itemHeight: number;
super(props); // function to turn an element of `items` into a react component
renderItem: (item: T) => JSX.Element;
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: number;
// the height of the viewport this content is scrolled in
height: number;
// all items for the list. These should not be react components, see `renderItem`.
items?: T[];
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin?: number;
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems?: number;
this.state = {}; element?: string;
className?: string;
} }
static getDerivedStateFromProps(props, state) { interface IState {
renderRange: ItemRange;
}
@replaceableComponent("views.elements.LazyRenderList")
export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
public static defaultProps: Partial<IProps<unknown>> = {
overflowItems: 20,
overflowMargin: 5,
};
constructor(props: IProps<T>) {
super(props);
this.state = {
renderRange: null,
};
}
public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> {
const range = LazyRenderList.getVisibleRangeFromProps(props); const range = LazyRenderList.getVisibleRangeFromProps(props);
const intersectRange = range.expand(props.overflowMargin); const intersectRange = range.expand(props.overflowMargin);
const renderRange = range.expand(props.overflowItems); const renderRange = range.expand(props.overflowItems);
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
return null; return null;
} }
static getVisibleRangeFromProps(props) { private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
const { items, itemHeight, scrollTop, height } = props; const { items, itemHeight, scrollTop, height } = props;
const length = items ? items.length : 0; const length = items ? items.length : 0;
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
return new ItemRange(topCount, renderCount, bottomCount); return new ItemRange(topCount, renderCount, bottomCount);
} }
render() { public render(): JSX.Element {
const { itemHeight, items, renderItem } = this.props; const { itemHeight, items, renderItem } = this.props;
const { renderRange } = this.state; const { renderRange } = this.state;
const { topCount, renderCount, bottomCount } = renderRange; const { topCount, renderCount, bottomCount } = renderRange;
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
} }
} }
LazyRenderList.defaultProps = {
overflowItems: 20,
overflowMargin: 5,
};
LazyRenderList.propTypes = {
// height in pixels of the component returned by `renderItem`
itemHeight: PropTypes.number.isRequired,
// function to turn an element of `items` into a react component
renderItem: PropTypes.func.isRequired,
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
scrollTop: PropTypes.number.isRequired,
// the height of the viewport this content is scrolled in
height: PropTypes.number.isRequired,
// all items for the list. These should not be react components, see `renderItem`.
items: PropTypes.array,
// the amount of items to scroll before causing a rerender,
// should typically be less than `overflowItems` unless applying
// margins in the parent component when using multiple LazyRenderList in one viewport.
// use 0 to only rerender when items will come into view.
overflowMargin: PropTypes.number,
// the amount of items to add at the top and bottom to render,
// so not every scroll of causes a rerender.
overflowItems: PropTypes.number,
};

View file

@ -16,25 +16,26 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { throttle } from "lodash"; import { throttle } from "lodash";
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from "../../../dispatcher/payloads";
export const getPersistKey = (appId: string) => 'widget_' + appId;
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body. // pass in a custom control as the actual body.
function getContainer(containerId) { function getContainer(containerId: string): HTMLDivElement {
return document.getElementById(containerId); return document.getElementById(containerId) as HTMLDivElement;
} }
function getOrCreateContainer(containerId) { function getOrCreateContainer(containerId: string): HTMLDivElement {
let container = getContainer(containerId); let container = getContainer(containerId);
if (!container) { if (!container) {
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
return container; return container;
} }
/* interface IProps {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use
// the same DOM container.
persistKey: string;
// z-index for the element. Defaults to 9.
zIndex?: number;
style?: React.StyleHTMLAttributes<HTMLDivElement>;
}
/**
* Class of component that renders its children in a separate ReactDOM virtual tree * Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body. * in a container element appended to document.body.
* *
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
* bounding rect as the parent of PE. * bounding rect as the parent of PE.
*/ */
@replaceableComponent("views.elements.PersistedElement") @replaceableComponent("views.elements.PersistedElement")
export default class PersistedElement extends React.Component { export default class PersistedElement extends React.Component<IProps> {
static propTypes = { private resizeObserver: ResizeObserver;
// Unique identifier for this PersistedElement instance private dispatcherRef: string;
// Any PersistedElements with the same persistKey will use private childContainer: HTMLDivElement;
// the same DOM container. private child: HTMLDivElement;
persistKey: PropTypes.string.isRequired,
// z-index for the element. Defaults to 9. constructor(props: IProps) {
zIndex: PropTypes.number, super(props);
};
constructor() { this.resizeObserver = new ResizeObserver(this.repositionChild);
super();
this.collectChildContainer = this.collectChildContainer.bind(this);
this.collectChild = this.collectChild.bind(this);
this._repositionChild = this._repositionChild.bind(this);
this._onAction = this._onAction.bind(this);
this.resizeObserver = new ResizeObserver(this._repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care // Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its // about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent // dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and // for this, so we bodge it by listening for document resize and
// the timeline_resize action. // the timeline_resize action.
window.addEventListener('resize', this._repositionChild); window.addEventListener('resize', this.repositionChild);
this._dispatcherRef = dis.register(this._onAction); this.dispatcherRef = dis.register(this.onAction);
} }
/** /**
* Removes the DOM elements created when a PersistedElement with the given * Removes the DOM elements created when a PersistedElement with the given
* persistKey was mounted. The DOM elements will be re-added if another * persistKey was mounted. The DOM elements will be re-added if another
* PeristedElement is mounted in the future. * PersistedElement is mounted in the future.
* *
* @param {string} persistKey Key used to uniquely identify this PersistedElement * @param {string} persistKey Key used to uniquely identify this PersistedElement
*/ */
static destroyElement(persistKey) { public static destroyElement(persistKey: string): void {
const container = getContainer('mx_persistedElement_' + persistKey); const container = getContainer('mx_persistedElement_' + persistKey);
if (container) { if (container) {
container.remove(); container.remove();
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
return Boolean(getContainer('mx_persistedElement_' + persistKey)); return Boolean(getContainer('mx_persistedElement_' + persistKey));
} }
collectChildContainer(ref) { private collectChildContainer = (ref: HTMLDivElement): void => {
if (this.childContainer) { if (this.childContainer) {
this.resizeObserver.unobserve(this.childContainer); this.resizeObserver.unobserve(this.childContainer);
} }
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
if (ref) { if (ref) {
this.resizeObserver.observe(ref); this.resizeObserver.observe(ref);
} }
} };
collectChild(ref) { private collectChild = (ref: HTMLDivElement): void => {
this.child = ref; this.child = ref;
this.updateChild(); this.updateChild();
} };
componentDidMount() { public componentDidMount(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentDidUpdate() { public componentDidUpdate(): void {
this.updateChild(); this.updateChild();
this.renderApp(); this.renderApp();
} }
componentWillUnmount() { public componentWillUnmount(): void {
this.updateChildVisibility(this.child, false); this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect(); this.resizeObserver.disconnect();
window.removeEventListener('resize', this._repositionChild); window.removeEventListener('resize', this.repositionChild);
dis.unregister(this._dispatcherRef); dis.unregister(this.dispatcherRef);
} }
_onAction(payload) { private onAction = (payload: ActionPayload): void => {
if (payload.action === 'timeline_resize') { if (payload.action === 'timeline_resize') {
this._repositionChild(); this.repositionChild();
} else if (payload.action === 'logout') { } else if (payload.action === 'logout') {
PersistedElement.destroyElement(this.props.persistKey); PersistedElement.destroyElement(this.props.persistKey);
} }
} };
_repositionChild() { private repositionChild = (): void => {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
} };
updateChild() { private updateChild(): void {
this.updateChildPosition(this.child, this.childContainer); this.updateChildPosition(this.child, this.childContainer);
this.updateChildVisibility(this.child, true); this.updateChildVisibility(this.child, true);
} }
renderApp() { private renderApp(): void {
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}> const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
<div ref={this.collectChild} style={this.props.style}> <div ref={this.collectChild} style={this.props.style}>
{ this.props.children } { this.props.children }
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey)); ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
} }
updateChildVisibility(child, visible) { private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
if (!child) return; if (!child) return;
child.style.display = visible ? 'block' : 'none'; child.style.display = visible ? 'block' : 'none';
} }
updateChildPosition = throttle((child, parent) => { private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return; if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect(); const parentRect = parent.getBoundingClientRect();
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
}); });
}, 100, { trailing: true, leading: true }); }, 100, { trailing: true, leading: true });
render() { public render(): JSX.Element {
return <div ref={this.collectChildContainer} />; return <div ref={this.collectChildContainer} />;
} }
} }
export const getPersistKey = (appId) => 'widget_' + appId;

View file

@ -19,47 +19,60 @@ import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { EventSubscription } from 'fbemitter';
import AppTile from "./AppTile";
import { Room } from "matrix-js-sdk/src/models/room";
interface IState {
roomId: string;
persistentWidgetId: string;
}
@replaceableComponent("views.elements.PersistentApp") @replaceableComponent("views.elements.PersistentApp")
export default class PersistentApp extends React.Component { export default class PersistentApp extends React.Component<{}, IState> {
state = { private roomStoreToken: EventSubscription;
constructor() {
super({});
this.state = {
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
}; };
componentDidMount() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
} }
componentWillUnmount() { public componentDidMount(): void {
if (this._roomStoreToken) { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
this._roomStoreToken.remove(); ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
} }
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
public componentWillUnmount(): void {
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
} }
} }
_onRoomViewStoreUpdate = payload => { private onRoomViewStoreUpdate = (): void => {
if (RoomViewStore.getRoomId() === this.state.roomId) return; if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({ this.setState({
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
}); });
}; };
_onActiveWidgetStoreUpdate = () => { private onActiveWidgetStoreUpdate = (): void => {
this.setState({ this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
}); });
}; };
_onMyMembership = async (room, membership) => { private onMyMembership = async (room: Room, membership: string): Promise<void> => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") { if (membership !== "join") {
// we're not in the room anymore - delete // we're not in the room anymore - delete
@ -69,7 +82,7 @@ export default class PersistentApp extends React.Component {
} }
}; };
render() { public render(): JSX.Element {
if (this.state.persistentWidgetId) { if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(), persistentWidgetInRoomId, appEvent.getId(),
); );
const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile return <AppTile
key={app.id} key={app.id}
app={app} app={app}

View file

@ -15,40 +15,52 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Field from "./Field"; import Field from "./Field";
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.PowerSelector") const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
export default class PowerSelector extends React.Component {
static propTypes = { interface IProps {
value: PropTypes.number.isRequired, value: number;
// The maximum value that can be set with the power selector // The maximum value that can be set with the power selector
maxValue: PropTypes.number.isRequired, maxValue: number;
// Default user power level for the room // Default user power level for the room
usersDefault: PropTypes.number.isRequired, usersDefault: number;
// should the user be able to change the value? false by default. // should the user be able to change the value? false by default.
disabled: PropTypes.bool, disabled?: boolean;
onChange: PropTypes.func, onChange?: (value: number, powerLevelKey: string) => void;
// Optional key to pass as the second argument to `onChange` // Optional key to pass as the second argument to `onChange`
powerLevelKey: PropTypes.string, powerLevelKey?: string;
// The name to annotate the selector with // The name to annotate the selector with
label: PropTypes.string, label?: string;
} }
static defaultProps = { interface IState {
levelRoleMap: {};
// List of power levels to show in the drop-down
options: number[];
customValue: number;
selectValue: number | string;
custom?: boolean;
customLevel?: number;
}
@replaceableComponent("views.elements.PowerSelector")
export default class PowerSelector extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
maxValue: Infinity, maxValue: Infinity,
usersDefault: 0, usersDefault: 0,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillMount() { public UNSAFE_componentWillMount(): void {
this._initStateFromProps(this.props); this.initStateFromProps(this.props);
} }
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
UNSAFE_componentWillReceiveProps(newProps) { public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
this._initStateFromProps(newProps); this.initStateFromProps(newProps);
} }
_initStateFromProps(newProps) { private initStateFromProps(newProps: IProps): void {
// This needs to be done now because levelRoleMap has translated strings // This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter(level => { const options = Object.keys(levelRoleMap).filter(level => {
return ( return (
level === undefined || level === undefined ||
level <= newProps.maxValue || parseInt(level) <= newProps.maxValue ||
level == newProps.value parseInt(level) == newProps.value
); );
}); }).map(level => parseInt(level));
const isCustom = levelRoleMap[newProps.value] === undefined; const isCustom = levelRoleMap[newProps.value] === undefined;
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
options, options,
custom: isCustom, custom: isCustom,
customLevel: newProps.value, customLevel: newProps.value,
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
}); });
} }
onSelectChange = event => { private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; const isCustom = event.target.value === CUSTOM_VALUE;
if (isCustom) { if (isCustom) {
this.setState({ custom: true }); this.setState({ custom: true });
} else { } else {
this.props.onChange(event.target.value, this.props.powerLevelKey); const powerLevel = parseInt(event.target.value);
this.setState({ selectValue: event.target.value }); this.props.onChange(powerLevel, this.props.powerLevelKey);
this.setState({ selectValue: powerLevel });
} }
}; };
onCustomChange = event => { private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ customValue: event.target.value }); this.setState({ customValue: parseInt(event.target.value) });
}; };
onCustomBlur = event => { private onCustomBlur = (event: React.FocusEvent): void => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); this.props.onChange(this.state.customValue, this.props.powerLevelKey);
}; };
onCustomKeyDown = event => { private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === Key.ENTER) { if (event.key === Key.ENTER) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
// raising a dialog which causes a blur which causes a dialog which causes a blur and // raising a dialog which causes a blur which causes a dialog which causes a blur and
// so on. By not causing the onChange to be called here, we avoid the loop because we // so on. By not causing the onChange to be called here, we avoid the loop because we
// handle the onBlur safely. // handle the onBlur safely.
event.target.blur(); (event.target as HTMLInputElement).blur();
} }
}; };
render() { public render(): JSX.Element {
let picker; let picker;
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
if (this.state.custom) { if (this.state.custom) {
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
); );
} else { } else {
// Each level must have a definition in this.state.levelRoleMap // Each level must have a definition in this.state.levelRoleMap
let options = this.state.options.map((level) => { const options = this.state.options.map((level) => {
return { return {
value: level, value: String(level),
text: Roles.textualPowerLevel(level, this.props.usersDefault), text: Roles.textualPowerLevel(level, this.props.usersDefault),
}; };
}); });
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
options = options.map((op) => { const optionsElements = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
value={String(this.state.selectValue)} value={String(this.state.selectValue)}
disabled={this.props.disabled} disabled={this.props.disabled}
> >
{ options } { optionsElements }
</Field> </Field>
); );
} }

View file

@ -17,25 +17,34 @@
import React from 'react'; import React from 'react';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason?: string;
contentHtml: string;
}
interface IState {
visible: boolean;
}
@replaceableComponent("views.elements.Spoiler") @replaceableComponent("views.elements.Spoiler")
export default class Spoiler extends React.Component { export default class Spoiler extends React.Component<IProps, IState> {
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
visible: false, visible: false,
}; };
} }
toggleVisible(e) { private toggleVisible = (e: React.MouseEvent): void => {
if (!this.state.visible) { if (!this.state.visible) {
// we are un-blurring, we don't want this click to propagate to potential child pills // we are un-blurring, we don't want this click to propagate to potential child pills
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
this.setState({ visible: !this.state.visible }); this.setState({ visible: !this.state.visible });
} };
render() { public render(): JSX.Element {
const reason = this.props.reason ? ( const reason = this.props.reason ? (
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span> <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
) : null; ) : null;
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
// as such, we pass the this.props.contentHtml instead and then set the raw // as such, we pass the this.props.contentHtml instead and then set the raw
// HTML content. This is secure as the contents have already been parsed previously // HTML content. This is secure as the contents have already been parsed previously
return ( return (
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}> <span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
{ reason } { reason }
&nbsp; &nbsp;
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} /> <span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />

View file

@ -15,40 +15,40 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { highlightBlock } from 'highlight.js'; import { highlightBlock } from 'highlight.js';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
className?: string;
children?: React.ReactNode;
}
@replaceableComponent("views.elements.SyntaxHighlight") @replaceableComponent("views.elements.SyntaxHighlight")
export default class SyntaxHighlight extends React.Component { export default class SyntaxHighlight extends React.Component<IProps> {
static propTypes = { private el: HTMLPreElement = null;
className: PropTypes.string,
children: PropTypes.node,
};
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._ref = this._ref.bind(this);
} }
// componentDidUpdate used here for reusability // componentDidUpdate used here for reusability
componentDidUpdate() { public componentDidUpdate(): void {
if (this._el) highlightBlock(this._el); if (this.el) highlightBlock(this.el);
} }
// call componentDidUpdate because _ref is fired on initial render // call componentDidUpdate because _ref is fired on initial render
// which does not fire componentDidUpdate // which does not fire componentDidUpdate
_ref(el) { private ref = (el: HTMLPreElement): void => {
this._el = el; this.el = el;
this.componentDidUpdate(); this.componentDidUpdate();
} };
render() { public render(): JSX.Element {
const { className, children } = this.props; const { className, children } = this.props;
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}> return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
<code>{ children }</code> <code>{ children }</code>
</pre>; </pre>;
} }
} }

View file

@ -15,42 +15,44 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "./Tooltip";
interface IProps {
class?: string;
tooltipClass?: string;
tooltip: React.ReactNode;
tooltipProps?: {};
onClick?: (ev?: React.MouseEvent) => void;
}
interface IState {
hover: boolean;
}
@replaceableComponent("views.elements.TextWithTooltip") @replaceableComponent("views.elements.TextWithTooltip")
export default class TextWithTooltip extends React.Component { export default class TextWithTooltip extends React.Component<IProps, IState> {
static propTypes = { constructor(props: IProps) {
class: PropTypes.string, super(props);
tooltipClass: PropTypes.string,
tooltip: PropTypes.node.isRequired,
tooltipProps: PropTypes.object,
};
constructor() {
super();
this.state = { this.state = {
hover: false, hover: false,
}; };
} }
onMouseOver = () => { private onMouseOver = (): void => {
this.setState({ hover: true }); this.setState({ hover: true });
}; };
onMouseLeave = () => { private onMouseLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
}; };
render() { public render(): JSX.Element {
const Tooltip = sdk.getComponent("elements.Tooltip");
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
return ( return (
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}> <span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
{ children } { children }
{ this.state.hover && <Tooltip { this.state.hover && <Tooltip
{...tooltipProps} {...tooltipProps}

View file

@ -15,20 +15,20 @@ 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";
import QRCode from "../QRCode"; import QRCode from "../QRCode";
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
interface IProps {
qrCodeData: QRCodeData;
}
@replaceableComponent("views.elements.crypto.VerificationQRCode") @replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent { export default class VerificationQRCode extends React.PureComponent<IProps> {
static propTypes = { public render(): JSX.Element {
qrCodeData: PropTypes.object.isRequired,
};
render() {
return ( return (
<QRCode <QRCode
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]} data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
className="mx_VerificationQRCode" className="mx_VerificationQRCode"
width={196} /> width={196} />
); );

View file

@ -1052,8 +1052,7 @@ const PowerLevelEditor: React.FC<{
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
const onPowerChange = useCallback(async (powerLevelStr: string) => { const onPowerChange = useCallback(async (powerLevel: number) => {
const powerLevel = parseInt(powerLevelStr, 10);
setSelectedPowerLevel(powerLevel); setSelectedPowerLevel(powerLevel);
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {

View file

@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
<AppTile <AppTile
app={app} app={app}
fullWidth fullWidth
show
showMenubar={false} showMenubar={false}
room={room} room={room}
userId={cli.getUserId()} userId={cli.getUserId()}

View file

@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ActionPayload } from '../../../dispatcher/payloads'; import { ActionPayload } from '../../../dispatcher/payloads';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
import { IApp } from "../../../stores/WidgetStore";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu. // We sit in a context menu, so this should be given to the context menu.
@ -256,12 +257,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack"); stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
// FIXME: could this use the same code as other apps? // FIXME: could this use the same code as other apps?
const stickerApp = { const stickerApp: IApp = {
id: stickerpickerWidget.id, id: stickerpickerWidget.id,
url: stickerpickerWidget.content.url, url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name, name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type, type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data, data: stickerpickerWidget.content.data,
roomId: stickerpickerWidget.content.roomId,
eventId: stickerpickerWidget.content.eventId,
avatar_url: stickerpickerWidget.content.avatar_url,
creatorUserId: stickerpickerWidget.content.creatorUserId,
}; };
stickersContent = ( stickersContent = (
@ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
onEditClick={this.launchManageIntegrations} onEditClick={this.launchManageIntegrations}
onDeleteClick={this.removeStickerpickerWidgets} onDeleteClick={this.removeStickerpickerWidgets}
showTitle={false} showTitle={false}
showCancel={false}
showPopout={false} showPopout={false}
onMinimiseClick={this.onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
userWidget={true} userWidget={true}
/> />
@ -345,16 +348,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
}); });
}; };
/**
* Trigger hiding of the sticker picker overlay
* @param {Event} ev Event that triggered the function call
*/
private onHideStickersClick = (ev: React.MouseEvent): void => {
if (this.props.showStickers) {
this.props.setShowStickers(false);
}
};
/** /**
* Called when the window is resized * Called when the window is resized
*/ */

View file

@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
} }
} }
private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => { private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
const eventsLevelPrefix = "event_levels_"; const eventsLevelPrefix = "event_levels_";
const value = parseInt(inputValue);
if (powerLevelKey.startsWith(eventsLevelPrefix)) { if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy // deep copy "events" object, Object.assign itself won't deep copy
plContent["events"] = Object.assign({}, plContent["events"] || {}); plContent["events"] = Object.assign({}, plContent["events"] || {});
@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
}); });
}; };
private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => { private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');

View file

@ -67,7 +67,7 @@ interface IAppTileProps {
userId: string; userId: string;
creatorUserId: string; creatorUserId: string;
waitForIframeLoad: boolean; waitForIframeLoad: boolean;
whitelistCapabilities: string[]; whitelistCapabilities?: string[];
userWidget: boolean; userWidget: boolean;
} }