Merge pull request #6815 from SimonBrandner/task/elements-ts
Convert `/src/components/views/elements` to TSpull/21833/head
						commit
						2eea606442
					
				|  | @ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel"; | |||
| import Field from '../../../../components/views/elements/Field'; | ||||
| import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; | ||||
| import DialogButtons from "../../../../components/views/elements/DialogButtons"; | ||||
| import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     onFinished: (confirmed: boolean) => void; | ||||
| } | ||||
| interface IProps extends IDialogProps {} | ||||
| 
 | ||||
| interface IState { | ||||
|     eventIndexSize: number; | ||||
|  |  | |||
|  | @ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => { | |||
|             <AppTile | ||||
|                 app={app} | ||||
|                 fullWidth | ||||
|                 show | ||||
|                 showMenubar={false} | ||||
|                 userWidget | ||||
|                 userId={cli.getUserId()} | ||||
|  |  | |||
|  | @ -23,10 +23,9 @@ import Modal from '../../../Modal'; | |||
| import BaseDialog from "./BaseDialog"; | ||||
| import DialogButtons from "../elements/DialogButtons"; | ||||
| import QuestionDialog from "./QuestionDialog"; | ||||
| import { IDialogProps } from "./IDialogProps"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     onFinished: (success: boolean) => void; | ||||
| } | ||||
| interface IProps extends IDialogProps {} | ||||
| 
 | ||||
| const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => { | ||||
|     const brand = SdkConfig.get().brand; | ||||
|  |  | |||
|  | @ -19,7 +19,6 @@ limitations under the License. | |||
| 
 | ||||
| import url from 'url'; | ||||
| import React, { createRef } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { MatrixClientPeg } from '../../../MatrixClientPeg'; | ||||
| import AccessibleButton from './AccessibleButton'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
|  | @ -39,33 +38,95 @@ import { MatrixCapabilities } from "matrix-widget-api"; | |||
| import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu"; | ||||
| import WidgetAvatar from "../avatars/WidgetAvatar"; | ||||
| 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") | ||||
| export default class AppTile extends React.Component { | ||||
|     constructor(props) { | ||||
| export default class AppTile extends React.Component<IProps, IState> { | ||||
|     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); | ||||
| 
 | ||||
|         // The key used for PersistedElement
 | ||||
|         this._persistKey = getPersistKey(this.props.app.id); | ||||
|         this.persistKey = getPersistKey(this.props.app.id); | ||||
|         try { | ||||
|             this._sgWidget = new StopGapWidget(this.props); | ||||
|             this._sgWidget.on("preparing", this._onWidgetPrepared); | ||||
|             this._sgWidget.on("ready", this._onWidgetReady); | ||||
|             this.sgWidget = new StopGapWidget(this.props); | ||||
|             this.sgWidget.on("preparing", this.onWidgetPrepared); | ||||
|             this.sgWidget.on("ready", this.onWidgetReady); | ||||
|         } catch (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._contextMenuButton = createRef(); | ||||
|         this.state = this.getNewState(props); | ||||
| 
 | ||||
|         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
 | ||||
|     hasPermissionToLoad = (props) => { | ||||
|         if (this._usingLocalWidget()) return true; | ||||
|     private hasPermissionToLoad = (props: IProps): boolean => { | ||||
|         if (this.usingLocalWidget()) return true; | ||||
|         if (!props.room) return true; // user widgets always have permissions
 | ||||
| 
 | ||||
|         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 | ||||
|      * @return {Object} Updated component state to be set with setState | ||||
|      */ | ||||
|     _getNewState(newProps) { | ||||
|     private getNewState(newProps: IProps): IState { | ||||
|         return { | ||||
|             initialising: true, // True while we are mangling the widget URL
 | ||||
|             // 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
 | ||||
|             // added it to the room, or if explicitly granted by the user
 | ||||
|             hasPermissionToLoad: this.hasPermissionToLoad(newProps), | ||||
|             error: null, | ||||
|             widgetPageTitle: newProps.widgetPageTitle, | ||||
|             menuDisplayed: false, | ||||
|             widgetPageTitle: this.props.widgetPageTitle, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     onAllowedWidgetsChange = () => { | ||||
|     private onAllowedWidgetsChange = (): void => { | ||||
|         const hasPermissionToLoad = this.hasPermissionToLoad(this.props); | ||||
| 
 | ||||
|         if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { | ||||
|             // Force the widget to be non-persistent (able to be deleted/forgotten)
 | ||||
|             ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); | ||||
|             PersistedElement.destroyElement(this._persistKey); | ||||
|             if (this._sgWidget) this._sgWidget.stop(); | ||||
|             PersistedElement.destroyElement(this.persistKey); | ||||
|             if (this.sgWidget) this.sgWidget.stop(); | ||||
|         } | ||||
| 
 | ||||
|         this.setState({ hasPermissionToLoad }); | ||||
|     }; | ||||
| 
 | ||||
|     isMixedContent() { | ||||
|     private isMixedContent(): boolean { | ||||
|         const parentContentProtocol = window.location.protocol; | ||||
|         const u = url.parse(this.props.app.url); | ||||
|         const childContentProtocol = u.protocol; | ||||
|  | @ -120,69 +181,70 @@ export default class AppTile extends React.Component { | |||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|     public componentDidMount(): void { | ||||
|         // Only fetch IM token on mount if we're showing and have permission to load
 | ||||
|         if (this._sgWidget && this.state.hasPermissionToLoad) { | ||||
|             this._startWidget(); | ||||
|         if (this.sgWidget && this.state.hasPermissionToLoad) { | ||||
|             this.startWidget(); | ||||
|         } | ||||
| 
 | ||||
|         // Widget action listeners
 | ||||
|         this.dispatcherRef = dis.register(this._onAction); | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|     public componentWillUnmount(): void { | ||||
|         // Widget action listeners
 | ||||
|         if (this.dispatcherRef) dis.unregister(this.dispatcherRef); | ||||
| 
 | ||||
|         // if it's not remaining on screen, get rid of the PersistedElement container
 | ||||
|         if (!ActiveWidgetStore.getWidgetPersistence(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(); | ||||
|         } | ||||
| 
 | ||||
|         SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); | ||||
|         SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef); | ||||
|     } | ||||
| 
 | ||||
|     _resetWidget(newProps) { | ||||
|         if (this._sgWidget) { | ||||
|             this._sgWidget.stop(); | ||||
|     private resetWidget(newProps: IProps): void { | ||||
|         if (this.sgWidget) { | ||||
|             this.sgWidget.stop(); | ||||
|         } | ||||
|         try { | ||||
|             this._sgWidget = new StopGapWidget(newProps); | ||||
|             this._sgWidget.on("preparing", this._onWidgetPrepared); | ||||
|             this._sgWidget.on("ready", this._onWidgetReady); | ||||
|             this._startWidget(); | ||||
|             this.sgWidget = new StopGapWidget(newProps); | ||||
|             this.sgWidget.on("preparing", this.onWidgetPrepared); | ||||
|             this.sgWidget.on("ready", this.onWidgetReady); | ||||
|             this.startWidget(); | ||||
|         } catch (e) { | ||||
|             console.log("Failed to construct widget", e); | ||||
|             this._sgWidget = null; | ||||
|             this.sgWidget = null; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _startWidget() { | ||||
|         this._sgWidget.prepare().then(() => { | ||||
|     private startWidget(): void { | ||||
|         this.sgWidget.prepare().then(() => { | ||||
|             this.setState({ initialising: false }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     _iframeRefChange = (ref) => { | ||||
|     private iframeRefChange = (ref: HTMLIFrameElement): void => { | ||||
|         this.iframe = ref; | ||||
|         if (ref) { | ||||
|             if (this._sgWidget) this._sgWidget.start(ref); | ||||
|             if (this.sgWidget) this.sgWidget.start(ref); | ||||
|         } else { | ||||
|             this._resetWidget(this.props); | ||||
|             this.resetWidget(this.props); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     // 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) { | ||||
|             this._getNewState(nextProps); | ||||
|             this.getNewState(nextProps); | ||||
|             if (this.state.hasPermissionToLoad) { | ||||
|                 this._resetWidget(nextProps); | ||||
|                 this.resetWidget(nextProps); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  | @ -198,7 +260,7 @@ export default class AppTile extends React.Component { | |||
|      * @private | ||||
|      * @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
 | ||||
|         // 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
 | ||||
|  | @ -217,27 +279,27 @@ export default class AppTile extends React.Component { | |||
|         } | ||||
| 
 | ||||
|         // Delete the widget from the persisted store for good measure.
 | ||||
|         PersistedElement.destroyElement(this._persistKey); | ||||
|         PersistedElement.destroyElement(this.persistKey); | ||||
|         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 }); | ||||
|     }; | ||||
| 
 | ||||
|     _onWidgetReady = () => { | ||||
|     private onWidgetReady = (): void => { | ||||
|         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) { | ||||
|             switch (payload.action) { | ||||
|                 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: 'stickerpicker_close' }); | ||||
|                     } else { | ||||
|  | @ -248,7 +310,7 @@ export default class AppTile extends React.Component { | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     _grantWidgetPermission = () => { | ||||
|     private grantWidgetPermission = (): void => { | ||||
|         const roomId = this.props.room.roomId; | ||||
|         console.info("Granting permission for widget to load: " + this.props.app.eventId); | ||||
|         const current = SettingsStore.getValue("allowedWidgets", roomId); | ||||
|  | @ -258,14 +320,14 @@ export default class AppTile extends React.Component { | |||
|             this.setState({ hasPermissionToLoad: true }); | ||||
| 
 | ||||
|             // Fetch a token for the integration manager, now that we're allowed to
 | ||||
|             this._startWidget(); | ||||
|             this.startWidget(); | ||||
|         }).catch(err => { | ||||
|             console.error(err); | ||||
|             // 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"; | ||||
|         if (this.props.app.name && 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 | ||||
|      * @returns {bool} true If using a local version of the widget | ||||
|      */ | ||||
|     _usingLocalWidget() { | ||||
|     private usingLocalWidget(): boolean { | ||||
|         return WidgetType.JITSI.matches(this.props.app.type); | ||||
|     } | ||||
| 
 | ||||
|     _getTileTitle() { | ||||
|     private getTileTitle(): JSX.Element { | ||||
|         const name = this.formatAppTileName(); | ||||
|         const titleSpacer = <span> - </span>; | ||||
|         let title = ''; | ||||
|  | @ -300,32 +362,32 @@ export default class AppTile extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     // 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
 | ||||
|         // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
 | ||||
|         if (WidgetType.JITSI.matches(this.props.app.type)) { | ||||
|             this._endWidgetActions().then(() => { | ||||
|             this.endWidgetActions().then(() => { | ||||
|                 if (this.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.
 | ||||
|         // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
 | ||||
|         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 }); | ||||
|     }; | ||||
| 
 | ||||
|     _closeContextMenu = () => { | ||||
|     private closeContextMenu = (): void => { | ||||
|         this.setState({ menuDisplayed: false }); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         let appTileBody; | ||||
| 
 | ||||
|         // 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...")} /> | ||||
|             </div> | ||||
|         ); | ||||
|         if (this._sgWidget === null) { | ||||
|         if (this.sgWidget === null) { | ||||
|             appTileBody = ( | ||||
|                 <div className={appTileBodyClass} style={appTileBodyStyles}> | ||||
|                     <AppWarning errorMsg={_t("Error loading Widget")} /> | ||||
|  | @ -365,9 +427,9 @@ export default class AppTile extends React.Component { | |||
|                     <AppPermission | ||||
|                         roomId={this.props.room.roomId} | ||||
|                         creatorUserId={this.props.creatorUserId} | ||||
|                         url={this._sgWidget.embedUrl} | ||||
|                         url={this.sgWidget.embedUrl} | ||||
|                         isRoomEncrypted={isEncrypted} | ||||
|                         onPermissionGranted={this._grantWidgetPermission} | ||||
|                         onPermissionGranted={this.grantWidgetPermission} | ||||
|                     /> | ||||
|                 </div> | ||||
|             ); | ||||
|  | @ -390,8 +452,8 @@ export default class AppTile extends React.Component { | |||
|                         { this.state.loading && loadingElement } | ||||
|                         <iframe | ||||
|                             allow={iframeFeatures} | ||||
|                             ref={this._iframeRefChange} | ||||
|                             src={this._sgWidget.embedUrl} | ||||
|                             ref={this.iframeRefChange} | ||||
|                             src={this.sgWidget.embedUrl} | ||||
|                             allowFullScreen={true} | ||||
|                             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
 | ||||
|                     // AppTile's border is in the wrong place
 | ||||
|                     appTileBody = <div className="mx_AppTile_persistedWrapper"> | ||||
|                         <PersistedElement persistKey={this._persistKey}> | ||||
|                         <PersistedElement persistKey={this.persistKey}> | ||||
|                             { appTileBody } | ||||
|                         </PersistedElement> | ||||
|                     </div>; | ||||
|  | @ -429,9 +491,9 @@ export default class AppTile extends React.Component { | |||
|         if (this.state.menuDisplayed) { | ||||
|             contextMenu = ( | ||||
|                 <RoomWidgetContextMenu | ||||
|                     {...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)} | ||||
|                     {...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)} | ||||
|                     app={this.props.app} | ||||
|                     onFinished={this._closeContextMenu} | ||||
|                     onFinished={this.closeContextMenu} | ||||
|                     showUnpin={!this.props.userWidget} | ||||
|                     userWidget={this.props.userWidget} | ||||
|                     onEditClick={this.props.onEditClick} | ||||
|  | @ -444,21 +506,21 @@ export default class AppTile extends React.Component { | |||
|             <div className={appTileClasses} id={this.props.app.id}> | ||||
|                 { this.props.showMenubar && | ||||
|                     <div className="mx_AppTileMenuBar"> | ||||
|                         <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}> | ||||
|                             { this.props.showTitle && this._getTileTitle() } | ||||
|                         <span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}> | ||||
|                             { this.props.showTitle && this.getTileTitle() } | ||||
|                         </span> | ||||
|                         <span className="mx_AppTileMenuBarWidgets"> | ||||
|                             { this.props.showPopout && <AccessibleButton | ||||
|                                 className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout" | ||||
|                                 title={_t('Popout widget')} | ||||
|                                 onClick={this._onPopoutWidgetClick} | ||||
|                                 onClick={this.onPopoutWidgetClick} | ||||
|                             /> } | ||||
|                             <ContextMenuButton | ||||
|                                 className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu" | ||||
|                                 label={_t("Options")} | ||||
|                                 isExpanded={this.state.menuDisplayed} | ||||
|                                 inputRef={this._contextMenuButton} | ||||
|                                 onClick={this._onContextMenuClick} | ||||
|                                 inputRef={this.contextMenuButton} | ||||
|                                 onClick={this.onContextMenuClick} | ||||
|                             /> | ||||
|                         </span> | ||||
|                     </div> } | ||||
|  | @ -469,49 +531,3 @@ export default class AppTile extends React.Component { | |||
|         </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, | ||||
| }; | ||||
|  | @ -1,24 +1,20 @@ | |||
| import React from 'react'; // eslint-disable-line no-unused-vars
 | ||||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| 
 | ||||
| const AppWarning = (props) => { | ||||
| interface IProps { | ||||
|     errorMsg?: string; | ||||
| } | ||||
| 
 | ||||
| const AppWarning: React.FC<IProps> = (props) => { | ||||
|     return ( | ||||
|         <div className='mx_AppPermissionWarning'> | ||||
|             <div className='mx_AppPermissionWarningImage'> | ||||
|                 <img src={require("../../../../res/img/warning.svg")} alt='' /> | ||||
|             </div> | ||||
|             <div className='mx_AppPermissionWarningText'> | ||||
|                 <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span> | ||||
|                 <span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span> | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| AppWarning.propTypes = { | ||||
|     errorMsg: PropTypes.string, | ||||
| }; | ||||
| AppWarning.defaultProps = { | ||||
|     errorMsg: 'Error', | ||||
| }; | ||||
| 
 | ||||
| export default AppWarning; | ||||
|  | @ -17,60 +17,61 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from "react"; | ||||
| import PropTypes from "prop-types"; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| 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. | ||||
|  */ | ||||
| @replaceableComponent("views.elements.DialogButtons") | ||||
| export default class DialogButtons extends React.Component { | ||||
|     static propTypes = { | ||||
|         // 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 = { | ||||
| export default class DialogButtons extends React.Component<IProps> { | ||||
|     public static defaultProps: Partial<IProps> = { | ||||
|         hasCancel: true, | ||||
|         disabled: false, | ||||
|     }; | ||||
| 
 | ||||
|     _onCancelClick = () => { | ||||
|         this.props.onCancel(); | ||||
|     private onCancelClick = (event: React.MouseEvent): void => { | ||||
|         this.props.onCancel(event); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         let primaryButtonClassName = "mx_Dialog_primary"; | ||||
|         if (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
 | ||||
|                 // primary in the DOM so will get form submissions unless we make it not a submit.
 | ||||
|                 type="button" | ||||
|                 onClick={this._onCancelClick} | ||||
|                 onClick={this.onCancelClick} | ||||
|                 className={this.props.cancelButtonClass} | ||||
|                 disabled={this.props.disabled} | ||||
|             > | ||||
|  | @ -14,71 +14,73 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as sdk from '../../../index'; | ||||
| import React, { ChangeEvent, createRef } from 'react'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| 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") | ||||
| export default class DirectorySearchBox extends React.Component { | ||||
|     constructor(props) { | ||||
|         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); | ||||
| export default class DirectorySearchBox extends React.Component<IProps, IState> { | ||||
|     private input = createRef<HTMLInputElement>(); | ||||
| 
 | ||||
|         this.input = null; | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             value: this.props.initialText || '', | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     _collectInput(e) { | ||||
|         this.input = e; | ||||
|     } | ||||
| 
 | ||||
|     _onClearClick() { | ||||
|     private onClearClick = (): void => { | ||||
|         this.setState({ value: '' }); | ||||
| 
 | ||||
|         if (this.input) { | ||||
|             this.input.focus(); | ||||
|         if (this.input.current) { | ||||
|             this.input.current.focus(); | ||||
| 
 | ||||
|             if (this.props.onClear) { | ||||
|                 this.props.onClear(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     _onChange(ev) { | ||||
|         if (!this.input) return; | ||||
|     private onChange = (ev: ChangeEvent<HTMLInputElement>): void => { | ||||
|         if (!this.input.current) return; | ||||
|         this.setState({ value: ev.target.value }); | ||||
| 
 | ||||
|         if (this.props.onChange) { | ||||
|             this.props.onChange(ev.target.value); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     _onKeyUp(ev) { | ||||
|     private onKeyUp = (ev: React.KeyboardEvent): void => { | ||||
|         if (ev.key == 'Enter' && this.props.showJoinButton) { | ||||
|             if (this.props.onJoinClick) { | ||||
|                 this.props.onJoinClick(this.state.value); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     _onJoinButtonClick() { | ||||
|     private onJoinButtonClick = (): void => { | ||||
|         if (this.props.onJoinClick) { | ||||
|             this.props.onJoinClick(this.state.value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); | ||||
|     }; | ||||
| 
 | ||||
|     public render(): JSX.Element { | ||||
|         const searchboxClasses = { | ||||
|             mx_DirectorySearchBox: true, | ||||
|         }; | ||||
|  | @ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component { | |||
|         let joinButton; | ||||
|         if (this.props.showJoinButton) { | ||||
|             joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton" | ||||
|                 onClick={this._onJoinButtonClick} | ||||
|                 onClick={this.onJoinButtonClick} | ||||
|             >{ _t("Join") }</AccessibleButton>; | ||||
|         } | ||||
| 
 | ||||
|  | @ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component { | |||
|                 name="dirsearch" | ||||
|                 value={this.state.value} | ||||
|                 className="mx_textinput_icon mx_textinput_search" | ||||
|                 ref={this._collectInput} | ||||
|                 onChange={this._onChange} | ||||
|                 onKeyUp={this._onKeyUp} | ||||
|                 ref={this.input} | ||||
|                 onChange={this.onChange} | ||||
|                 onKeyUp={this.onKeyUp} | ||||
|                 placeholder={this.props.placeholder} | ||||
|                 autoFocus | ||||
|             /> | ||||
|             { joinButton } | ||||
|             <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> | ||||
|             <AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} /> | ||||
|         </div>; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| DirectorySearchBox.propTypes = { | ||||
|     className: PropTypes.string, | ||||
|     onChange: PropTypes.func, | ||||
|     onClear: PropTypes.func, | ||||
|     onJoinClick: PropTypes.func, | ||||
|     placeholder: PropTypes.string, | ||||
|     showJoinButton: PropTypes.bool, | ||||
|     initialText: PropTypes.string, | ||||
| }; | ||||
|  | @ -16,33 +16,42 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React, { createRef } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { Key } from "../../../Keyboard"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| enum Phases { | ||||
|     Display = "display", | ||||
|     Edit = "edit", | ||||
| } | ||||
| 
 | ||||
| interface IProps { | ||||
|     onValueChanged?: (value: string, shouldSubmit: boolean) => void; | ||||
|     initialValue?: string; | ||||
|     label?: string; | ||||
|     placeholder?: string; | ||||
|     className?: string; | ||||
|     labelClassName?: string; | ||||
|     placeholderClassName?: string; | ||||
|     // Overrides blurToSubmit if true
 | ||||
|     blurToCancel?: boolean; | ||||
|     // Will cause onValueChanged(value, true) to fire on blur
 | ||||
|     blurToSubmit?: boolean; | ||||
|     editable?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     phase: Phases; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.EditableText") | ||||
| export default class EditableText extends React.Component { | ||||
|     static propTypes = { | ||||
|         onValueChanged: PropTypes.func, | ||||
|         initialValue: PropTypes.string, | ||||
|         label: PropTypes.string, | ||||
|         placeholder: PropTypes.string, | ||||
|         className: PropTypes.string, | ||||
|         labelClassName: PropTypes.string, | ||||
|         placeholderClassName: PropTypes.string, | ||||
|         // Overrides blurToSubmit if true
 | ||||
|         blurToCancel: PropTypes.bool, | ||||
|         // Will cause onValueChanged(value, true) to fire on blur
 | ||||
|         blurToSubmit: PropTypes.bool, | ||||
|         editable: PropTypes.bool, | ||||
|     }; | ||||
| 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>(); | ||||
| 
 | ||||
|     static Phases = { | ||||
|         Display: "display", | ||||
|         Edit: "edit", | ||||
|     }; | ||||
| 
 | ||||
|     static defaultProps = { | ||||
|     public static defaultProps: Partial<IProps> = { | ||||
|         onValueChanged() {}, | ||||
|         initialValue: '', | ||||
|         label: '', | ||||
|  | @ -53,81 +62,61 @@ export default class EditableText extends React.Component { | |||
|         blurToSubmit: false, | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         // we track value as an JS object field rather than in React state
 | ||||
|         // as React doesn't play nice with contentEditable.
 | ||||
|         this.value = ''; | ||||
|         this.placeholder = false; | ||||
| 
 | ||||
|         this._editable_div = createRef(); | ||||
|         this.state = { | ||||
|             phase: Phases.Display, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     state = { | ||||
|         phase: EditableText.Phases.Display, | ||||
|     }; | ||||
| 
 | ||||
|     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
 | ||||
|     // eslint-disable-next-line camelcase
 | ||||
|     UNSAFE_componentWillReceiveProps(nextProps) { | ||||
|     // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
 | ||||
|     public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { | ||||
|         if (nextProps.initialValue !== this.props.initialValue) { | ||||
|             this.value = nextProps.initialValue; | ||||
|             if (this._editable_div.current) { | ||||
|             if (this.editableDiv.current) { | ||||
|                 this.showPlaceholder(!this.value); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|     public componentDidMount(): void { | ||||
|         this.value = this.props.initialValue; | ||||
|         if (this._editable_div.current) { | ||||
|         if (this.editableDiv.current) { | ||||
|             this.showPlaceholder(!this.value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     showPlaceholder = show => { | ||||
|     private showPlaceholder = (show: boolean): void => { | ||||
|         if (show) { | ||||
|             this._editable_div.current.textContent = this.props.placeholder; | ||||
|             this._editable_div.current.setAttribute("class", this.props.className | ||||
|             this.editableDiv.current.textContent = this.props.placeholder; | ||||
|             this.editableDiv.current.setAttribute("class", this.props.className | ||||
|                 + " " + this.props.placeholderClassName); | ||||
|             this.placeholder = true; | ||||
|             this.value = ''; | ||||
|         } else { | ||||
|             this._editable_div.current.textContent = this.value; | ||||
|             this._editable_div.current.setAttribute("class", this.props.className); | ||||
|             this.editableDiv.current.textContent = this.value; | ||||
|             this.editableDiv.current.setAttribute("class", this.props.className); | ||||
|             this.placeholder = false; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     getValue = () => this.value; | ||||
| 
 | ||||
|     setValue = value => { | ||||
|         this.value = value; | ||||
|         this.showPlaceholder(!this.value); | ||||
|     }; | ||||
| 
 | ||||
|     edit = () => { | ||||
|     private cancelEdit = (): void => { | ||||
|         this.setState({ | ||||
|             phase: EditableText.Phases.Edit, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     cancelEdit = () => { | ||||
|         this.setState({ | ||||
|             phase: EditableText.Phases.Display, | ||||
|             phase: Phases.Display, | ||||
|         }); | ||||
|         this.value = this.props.initialValue; | ||||
|         this.showPlaceholder(!this.value); | ||||
|         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); | ||||
|     }; | ||||
| 
 | ||||
|     onKeyDown = ev => { | ||||
|     private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => { | ||||
|         // console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + 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);
 | ||||
|     }; | ||||
| 
 | ||||
|     onKeyUp = ev => { | ||||
|     private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => { | ||||
|         // 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); | ||||
|         } else if (!this.placeholder) { | ||||
|             this.value = ev.target.textContent; | ||||
|             this.value = (ev.target as HTMLDivElement).textContent; | ||||
|         } | ||||
| 
 | ||||
|         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);
 | ||||
|     }; | ||||
| 
 | ||||
|     onClickDiv = ev => { | ||||
|     private onClickDiv = (): void => { | ||||
|         if (!this.props.editable) return; | ||||
| 
 | ||||
|         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);
 | ||||
| 
 | ||||
|         const node = ev.target.childNodes[0]; | ||||
|         if (node) { | ||||
|             const range = document.createRange(); | ||||
|             range.setStart(node, 0); | ||||
|             range.setEnd(node, node.length); | ||||
|             range.setEnd(node, ev.target.childNodes.length); | ||||
| 
 | ||||
|             const sel = window.getSelection(); | ||||
|             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 submit = (ev.key === Key.ENTER) || shouldSubmit; | ||||
|         const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit; | ||||
|         this.setState({ | ||||
|             phase: EditableText.Phases.Display, | ||||
|             phase: Phases.Display, | ||||
|         }, () => { | ||||
|             if (this.value !== this.props.initialValue) { | ||||
|                 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(); | ||||
|         sel.removeAllRanges(); | ||||
| 
 | ||||
|  | @ -208,11 +201,11 @@ export default class EditableText extends React.Component { | |||
|         this.showPlaceholder(!this.value); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         const { className, editable, initialValue, label, labelClassName } = this.props; | ||||
|         let editableEl; | ||||
| 
 | ||||
|         if (!editable || (this.state.phase === EditableText.Phases.Display && | ||||
|         if (!editable || (this.state.phase === Phases.Display && | ||||
|             (label || labelClassName) && !this.value) | ||||
|         ) { | ||||
|             // show the label
 | ||||
|  | @ -222,7 +215,7 @@ export default class EditableText extends React.Component { | |||
|         } else { | ||||
|             // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
 | ||||
|             editableEl = <div | ||||
|                 ref={this._editable_div} | ||||
|                 ref={this.editableDiv} | ||||
|                 contentEditable={true} | ||||
|                 className={className} | ||||
|                 onKeyDown={this.onKeyDown} | ||||
|  | @ -15,9 +15,34 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as sdk from '../../../index'; | ||||
| 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 | ||||
|  | @ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; | |||
|  * taken from the 'initialValue' property. | ||||
|  */ | ||||
| @replaceableComponent("views.elements.EditableTextContainer") | ||||
| export default class EditableTextContainer extends React.Component { | ||||
|     constructor(props) { | ||||
| export default class EditableTextContainer extends React.Component<IProps, IState> { | ||||
|     private unmounted = false; | ||||
|     public static defaultProps: Partial<IProps> = { | ||||
|         initialValue: "", | ||||
|         placeholder: "", | ||||
|         blurToSubmit: false, | ||||
|         onSubmit: () => { return Promise.resolve(); }, | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this._unmounted = false; | ||||
|         this.state = { | ||||
|             busy: false, | ||||
|             errorString: null, | ||||
|             value: props.initialValue, | ||||
|         }; | ||||
|         this._onValueChanged = this._onValueChanged.bind(this); | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         if (this.props.getInitialValue === undefined) { | ||||
|             // use whatever was given in the initialValue property.
 | ||||
|             return; | ||||
|         } | ||||
|     public async componentDidMount(): Promise<void> { | ||||
|         // use whatever was given in the initialValue property.
 | ||||
|         if (this.props.getInitialValue === undefined) return; | ||||
| 
 | ||||
|         this.setState({ busy: true }); | ||||
| 
 | ||||
|         this.props.getInitialValue().then( | ||||
|             (result) => { | ||||
|                 if (this._unmounted) { return; } | ||||
|                 this.setState({ | ||||
|                     busy: false, | ||||
|                     value: result, | ||||
|                 }); | ||||
|             }, | ||||
|             (error) => { | ||||
|                 if (this._unmounted) { return; } | ||||
|                 this.setState({ | ||||
|                     errorString: error.toString(), | ||||
|                     busy: false, | ||||
|                 }); | ||||
|             }, | ||||
|         ); | ||||
|         try { | ||||
|             const initialValue = await this.props.getInitialValue(); | ||||
|             if (this.unmounted) return; | ||||
|             this.setState({ | ||||
|                 busy: false, | ||||
|                 value: initialValue, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             if (this.unmounted) return; | ||||
|             this.setState({ | ||||
|                 errorString: error.toString(), | ||||
|                 busy: false, | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         this._unmounted = true; | ||||
|     public componentWillUnmount(): void { | ||||
|         this.unmounted = true; | ||||
|     } | ||||
| 
 | ||||
|     _onValueChanged(value, shouldSubmit) { | ||||
|     private onValueChanged = (value: string, shouldSubmit: boolean): void => { | ||||
|         if (!shouldSubmit) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component { | |||
| 
 | ||||
|         this.props.onSubmit(value).then( | ||||
|             () => { | ||||
|                 if (this._unmounted) { return; } | ||||
|                 if (this.unmounted) { return; } | ||||
|                 this.setState({ | ||||
|                     busy: false, | ||||
|                     value: value, | ||||
|                 }); | ||||
|             }, | ||||
|             (error) => { | ||||
|                 if (this._unmounted) { return; } | ||||
|                 if (this.unmounted) { return; } | ||||
|                 this.setState({ | ||||
|                     errorString: error.toString(), | ||||
|                     busy: false, | ||||
|                 }); | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         if (this.state.busy) { | ||||
|             const Loader = sdk.getComponent("elements.Spinner"); | ||||
|             return ( | ||||
|                 <Loader /> | ||||
|                 <Spinner /> | ||||
|             ); | ||||
|         } else if (this.state.errorString) { | ||||
|             return ( | ||||
|                 <div className="error">{ this.state.errorString }</div> | ||||
|             ); | ||||
|         } else { | ||||
|             const EditableText = sdk.getComponent('elements.EditableText'); | ||||
|             return ( | ||||
|                 <EditableText initialValue={this.state.value} | ||||
|                     placeholder={this.props.placeholder} | ||||
|                     onValueChanged={this._onValueChanged} | ||||
|                     onValueChanged={this.onValueChanged} | ||||
|                     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(); }, | ||||
| }; | ||||
|  | @ -16,13 +16,13 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| 
 | ||||
| import * as sdk from '../../../index'; | ||||
| import * as languageHandler from '../../../languageHandler'; | ||||
| import SettingsStore from "../../../settings/SettingsStore"; | ||||
| import { _t } from "../../../languageHandler"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| import Spinner from "./Spinner"; | ||||
| import Dropdown from "./Dropdown"; | ||||
| 
 | ||||
| function languageMatchesSearchQuery(query, language) { | ||||
|     if (language.label.toUpperCase().includes(query.toUpperCase())) return true; | ||||
|  | @ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) { | |||
|     return false; | ||||
| } | ||||
| 
 | ||||
| interface IProps { | ||||
|     className?: string; | ||||
|     onOptionChange: (language: string) => void; | ||||
|     value?: string; | ||||
|     disabled?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     searchQuery: string; | ||||
|     langs: string[]; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.LanguageDropdown") | ||||
| export default class LanguageDropdown extends React.Component { | ||||
|     constructor(props) { | ||||
| export default class LanguageDropdown extends React.Component<IProps, IState> { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
|         this._onSearchChange = this._onSearchChange.bind(this); | ||||
| 
 | ||||
|         this.state = { | ||||
|             searchQuery: '', | ||||
|  | @ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component { | |||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|     public componentDidMount(): void { | ||||
|         languageHandler.getAllLanguagesFromJson().then((langs) => { | ||||
|             langs.sort(function(a, b) { | ||||
|                 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({ | ||||
|             searchQuery: search, | ||||
|         }); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         if (this.state.langs === null) { | ||||
|             const Spinner = sdk.getComponent('elements.Spinner'); | ||||
|             return <Spinner />; | ||||
|         } | ||||
| 
 | ||||
|         const Dropdown = sdk.getComponent('elements.Dropdown'); | ||||
| 
 | ||||
|         let displayedLanguages; | ||||
|         if (this.state.searchQuery) { | ||||
|             displayedLanguages = this.state.langs.filter((lang) => { | ||||
|  | @ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component { | |||
|             id="mx_LanguageDropdown" | ||||
|             className={this.props.className} | ||||
|             onOptionChange={this.props.onOptionChange} | ||||
|             onSearchChange={this._onSearchChange} | ||||
|             onSearchChange={this.onSearchChange} | ||||
|             searchEnabled={true} | ||||
|             value={value} | ||||
|             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, | ||||
| }; | ||||
|  | @ -15,17 +15,16 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from "react"; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| class ItemRange { | ||||
|     constructor(topCount, renderCount, bottomCount) { | ||||
|         this.topCount = topCount; | ||||
|         this.renderCount = renderCount; | ||||
|         this.bottomCount = bottomCount; | ||||
|     } | ||||
|     constructor( | ||||
|         public topCount: number, | ||||
|         public renderCount: number, | ||||
|         public bottomCount: number, | ||||
|     ) { } | ||||
| 
 | ||||
|     contains(range) { | ||||
|     public contains(range: ItemRange): boolean { | ||||
|         // don't contain empty ranges
 | ||||
|         // as it will prevent clearing the list
 | ||||
|         // once it is scrolled far enough out of view
 | ||||
|  | @ -36,7 +35,7 @@ class ItemRange { | |||
|             (range.topCount + range.renderCount) <= (this.topCount + this.renderCount); | ||||
|     } | ||||
| 
 | ||||
|     expand(amount) { | ||||
|     public expand(amount: number): ItemRange { | ||||
|         // don't expand ranges that won't render anything
 | ||||
|         if (this.renderCount === 0) { | ||||
|             return this; | ||||
|  | @ -51,20 +50,55 @@ class ItemRange { | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     totalSize() { | ||||
|     public totalSize(): number { | ||||
|         return this.topCount + this.renderCount + this.bottomCount; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| interface IProps<T> { | ||||
|     // height in pixels of the component returned by `renderItem`
 | ||||
|     itemHeight: number; | ||||
|     // 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; | ||||
| 
 | ||||
|     element?: string; | ||||
|     className?: string; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     renderRange: ItemRange; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.LazyRenderList") | ||||
| export default class LazyRenderList extends React.Component { | ||||
|     constructor(props) { | ||||
| 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 = {}; | ||||
|         this.state = { | ||||
|             renderRange: null, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     static getDerivedStateFromProps(props, state) { | ||||
|     public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> { | ||||
|         const range = LazyRenderList.getVisibleRangeFromProps(props); | ||||
|         const intersectRange = range.expand(props.overflowMargin); | ||||
|         const renderRange = range.expand(props.overflowItems); | ||||
|  | @ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component { | |||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     static getVisibleRangeFromProps(props) { | ||||
|     private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange { | ||||
|         const { items, itemHeight, scrollTop, height } = props; | ||||
|         const length = items ? items.length : 0; | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         const { itemHeight, items, renderItem } = this.props; | ||||
|         const { renderRange } = this.state; | ||||
|         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, | ||||
| }; | ||||
|  | @ -16,25 +16,26 @@ limitations under the License. | |||
| 
 | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { throttle } from "lodash"; | ||||
| import ResizeObserver from 'resize-observer-polyfill'; | ||||
| 
 | ||||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
| import { MatrixClientPeg } from "../../../MatrixClientPeg"; | ||||
| import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; | ||||
| 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
 | ||||
| // of doing reusable widgets like dialog boxes & menus where we go and
 | ||||
| // pass in a custom control as the actual body.
 | ||||
| 
 | ||||
| function getContainer(containerId) { | ||||
|     return document.getElementById(containerId); | ||||
| function getContainer(containerId: string): HTMLDivElement { | ||||
|     return document.getElementById(containerId) as HTMLDivElement; | ||||
| } | ||||
| 
 | ||||
| function getOrCreateContainer(containerId) { | ||||
| function getOrCreateContainer(containerId: string): HTMLDivElement { | ||||
|     let container = getContainer(containerId); | ||||
| 
 | ||||
|     if (!container) { | ||||
|  | @ -46,7 +47,19 @@ function getOrCreateContainer(containerId) { | |||
|     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 | ||||
|  * in a container element appended to document.body. | ||||
|  * | ||||
|  | @ -58,42 +71,33 @@ function getOrCreateContainer(containerId) { | |||
|  * bounding rect as the parent of PE. | ||||
|  */ | ||||
| @replaceableComponent("views.elements.PersistedElement") | ||||
| export default class PersistedElement extends React.Component { | ||||
|     static propTypes = { | ||||
|         // Unique identifier for this PersistedElement instance
 | ||||
|         // Any PersistedElements with the same persistKey will use
 | ||||
|         // the same DOM container.
 | ||||
|         persistKey: PropTypes.string.isRequired, | ||||
| export default class PersistedElement extends React.Component<IProps> { | ||||
|     private resizeObserver: ResizeObserver; | ||||
|     private dispatcherRef: string; | ||||
|     private childContainer: HTMLDivElement; | ||||
|     private child: HTMLDivElement; | ||||
| 
 | ||||
|         // z-index for the element. Defaults to 9.
 | ||||
|         zIndex: PropTypes.number, | ||||
|     }; | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|     constructor() { | ||||
|         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); | ||||
|         this.resizeObserver = new ResizeObserver(this.repositionChild); | ||||
|         // Annoyingly, a resize observer is insufficient, since we also care
 | ||||
|         // about when the element moves on the screen without changing its
 | ||||
|         // dimensions. Doesn't look like there's a ResizeObserver equivalent
 | ||||
|         // for this, so we bodge it by listening for document resize and
 | ||||
|         // the timeline_resize action.
 | ||||
|         window.addEventListener('resize', this._repositionChild); | ||||
|         this._dispatcherRef = dis.register(this._onAction); | ||||
|         window.addEventListener('resize', this.repositionChild); | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes the DOM elements created when a PersistedElement with the given | ||||
|      * 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 | ||||
|      */ | ||||
|     static destroyElement(persistKey) { | ||||
|     public static destroyElement(persistKey: string): void { | ||||
|         const container = getContainer('mx_persistedElement_' + persistKey); | ||||
|         if (container) { | ||||
|             container.remove(); | ||||
|  | @ -104,7 +108,7 @@ export default class PersistedElement extends React.Component { | |||
|         return Boolean(getContainer('mx_persistedElement_' + persistKey)); | ||||
|     } | ||||
| 
 | ||||
|     collectChildContainer(ref) { | ||||
|     private collectChildContainer = (ref: HTMLDivElement): void => { | ||||
|         if (this.childContainer) { | ||||
|             this.resizeObserver.unobserve(this.childContainer); | ||||
|         } | ||||
|  | @ -112,48 +116,48 @@ export default class PersistedElement extends React.Component { | |||
|         if (ref) { | ||||
|             this.resizeObserver.observe(ref); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     collectChild(ref) { | ||||
|     private collectChild = (ref: HTMLDivElement): void => { | ||||
|         this.child = ref; | ||||
|         this.updateChild(); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|     public componentDidMount(): void { | ||||
|         this.updateChild(); | ||||
|         this.renderApp(); | ||||
|     } | ||||
| 
 | ||||
|     componentDidUpdate() { | ||||
|     public componentDidUpdate(): void { | ||||
|         this.updateChild(); | ||||
|         this.renderApp(); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|     public componentWillUnmount(): void { | ||||
|         this.updateChildVisibility(this.child, false); | ||||
|         this.resizeObserver.disconnect(); | ||||
|         window.removeEventListener('resize', this._repositionChild); | ||||
|         dis.unregister(this._dispatcherRef); | ||||
|         window.removeEventListener('resize', this.repositionChild); | ||||
|         dis.unregister(this.dispatcherRef); | ||||
|     } | ||||
| 
 | ||||
|     _onAction(payload) { | ||||
|     private onAction = (payload: ActionPayload): void => { | ||||
|         if (payload.action === 'timeline_resize') { | ||||
|             this._repositionChild(); | ||||
|             this.repositionChild(); | ||||
|         } else if (payload.action === 'logout') { | ||||
|             PersistedElement.destroyElement(this.props.persistKey); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     _repositionChild() { | ||||
|     private repositionChild = (): void => { | ||||
|         this.updateChildPosition(this.child, this.childContainer); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     updateChild() { | ||||
|     private updateChild(): void { | ||||
|         this.updateChildPosition(this.child, this.childContainer); | ||||
|         this.updateChildVisibility(this.child, true); | ||||
|     } | ||||
| 
 | ||||
|     renderApp() { | ||||
|     private renderApp(): void { | ||||
|         const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}> | ||||
|             <div ref={this.collectChild} style={this.props.style}> | ||||
|                 { this.props.children } | ||||
|  | @ -163,12 +167,12 @@ export default class PersistedElement extends React.Component { | |||
|         ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey)); | ||||
|     } | ||||
| 
 | ||||
|     updateChildVisibility(child, visible) { | ||||
|     private updateChildVisibility(child: HTMLDivElement, visible: boolean): void { | ||||
|         if (!child) return; | ||||
|         child.style.display = visible ? 'block' : 'none'; | ||||
|     } | ||||
| 
 | ||||
|     updateChildPosition = throttle((child, parent) => { | ||||
|     private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => { | ||||
|         if (!child || !parent) return; | ||||
| 
 | ||||
|         const parentRect = parent.getBoundingClientRect(); | ||||
|  | @ -182,9 +186,8 @@ export default class PersistedElement extends React.Component { | |||
|         }); | ||||
|     }, 100, { trailing: true, leading: true }); | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         return <div ref={this.collectChildContainer} />; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export const getPersistKey = (appId) => 'widget_' + appId; | ||||
|  | @ -19,57 +19,70 @@ import React from 'react'; | |||
| import RoomViewStore from '../../../stores/RoomViewStore'; | ||||
| import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; | ||||
| import WidgetUtils from '../../../utils/WidgetUtils'; | ||||
| import * as sdk from '../../../index'; | ||||
| import { MatrixClientPeg } from '../../../MatrixClientPeg'; | ||||
| 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") | ||||
| export default class PersistentApp extends React.Component { | ||||
|     state = { | ||||
|         roomId: RoomViewStore.getRoomId(), | ||||
|         persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), | ||||
|     }; | ||||
| export default class PersistentApp extends React.Component<{}, IState> { | ||||
|     private roomStoreToken: EventSubscription; | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); | ||||
|         ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate); | ||||
|         MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership); | ||||
|     constructor() { | ||||
|         super({}); | ||||
| 
 | ||||
|         this.state = { | ||||
|             roomId: RoomViewStore.getRoomId(), | ||||
|             persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         if (this._roomStoreToken) { | ||||
|             this._roomStoreToken.remove(); | ||||
|     public componentDidMount(): void { | ||||
|         this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); | ||||
|         ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate); | ||||
|         MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); | ||||
|     } | ||||
| 
 | ||||
|     public componentWillUnmount(): void { | ||||
|         if (this.roomStoreToken) { | ||||
|             this.roomStoreToken.remove(); | ||||
|         } | ||||
|         ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate); | ||||
|         ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); | ||||
|         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; | ||||
|         this.setState({ | ||||
|             roomId: RoomViewStore.getRoomId(), | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _onActiveWidgetStoreUpdate = () => { | ||||
|     private onActiveWidgetStoreUpdate = (): void => { | ||||
|         this.setState({ | ||||
|             persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _onMyMembership = async (room, membership) => { | ||||
|     private onMyMembership = async (room: Room, membership: string): Promise<void> => { | ||||
|         const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); | ||||
|         if (membership !== "join") { | ||||
|             // we're not in the room anymore - delete
 | ||||
|             if (room.roomId === persistentWidgetInRoomId) { | ||||
|             if (room .roomId === persistentWidgetInRoomId) { | ||||
|                 ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         if (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(), | ||||
|                     persistentWidgetInRoomId, appEvent.getId(), | ||||
|                 ); | ||||
|                 const AppTile = sdk.getComponent('elements.AppTile'); | ||||
|                 return <AppTile | ||||
|                     key={app.id} | ||||
|                     app={app} | ||||
|  | @ -15,40 +15,52 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as Roles from '../../../Roles'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import Field from "./Field"; | ||||
| import { Key } from "../../../Keyboard"; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     value: number; | ||||
|     // The maximum value that can be set with the power selector
 | ||||
|     maxValue: number; | ||||
| 
 | ||||
|     // Default user power level for the room
 | ||||
|     usersDefault: number; | ||||
| 
 | ||||
|     // should the user be able to change the value? false by default.
 | ||||
|     disabled?: boolean; | ||||
|     onChange?: (value: number, powerLevelKey: string) => void; | ||||
| 
 | ||||
|     // Optional key to pass as the second argument to `onChange`
 | ||||
|     powerLevelKey?: string; | ||||
| 
 | ||||
|     // The name to annotate the selector with
 | ||||
|     label?: string; | ||||
| } | ||||
| 
 | ||||
| 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 { | ||||
|     static propTypes = { | ||||
|         value: PropTypes.number.isRequired, | ||||
|         // The maximum value that can be set with the power selector
 | ||||
|         maxValue: PropTypes.number.isRequired, | ||||
| 
 | ||||
|         // Default user power level for the room
 | ||||
|         usersDefault: PropTypes.number.isRequired, | ||||
| 
 | ||||
|         // should the user be able to change the value? false by default.
 | ||||
|         disabled: PropTypes.bool, | ||||
|         onChange: PropTypes.func, | ||||
| 
 | ||||
|         // Optional key to pass as the second argument to `onChange`
 | ||||
|         powerLevelKey: PropTypes.string, | ||||
| 
 | ||||
|         // The name to annotate the selector with
 | ||||
|         label: PropTypes.string, | ||||
|     } | ||||
| 
 | ||||
|     static defaultProps = { | ||||
| export default class PowerSelector extends React.Component<IProps, IState> { | ||||
|     public static defaultProps: Partial<IProps> = { | ||||
|         maxValue: Infinity, | ||||
|         usersDefault: 0, | ||||
|     }; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|  | @ -62,26 +74,26 @@ export default class PowerSelector extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
 | ||||
|     // eslint-disable-next-line camelcase
 | ||||
|     UNSAFE_componentWillMount() { | ||||
|         this._initStateFromProps(this.props); | ||||
|     // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
 | ||||
|     public UNSAFE_componentWillMount(): void { | ||||
|         this.initStateFromProps(this.props); | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line camelcase
 | ||||
|     UNSAFE_componentWillReceiveProps(newProps) { | ||||
|         this._initStateFromProps(newProps); | ||||
|     // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
 | ||||
|     public UNSAFE_componentWillReceiveProps(newProps: IProps): void { | ||||
|         this.initStateFromProps(newProps); | ||||
|     } | ||||
| 
 | ||||
|     _initStateFromProps(newProps) { | ||||
|     private initStateFromProps(newProps: IProps): void { | ||||
|         // This needs to be done now because levelRoleMap has translated strings
 | ||||
|         const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); | ||||
|         const options = Object.keys(levelRoleMap).filter(level => { | ||||
|             return ( | ||||
|                 level === undefined || | ||||
|                 level <= newProps.maxValue || | ||||
|                 level == newProps.value | ||||
|                 parseInt(level) <= newProps.maxValue || | ||||
|                 parseInt(level) == newProps.value | ||||
|             ); | ||||
|         }); | ||||
|         }).map(level => parseInt(level)); | ||||
| 
 | ||||
|         const isCustom = levelRoleMap[newProps.value] === undefined; | ||||
| 
 | ||||
|  | @ -90,32 +102,33 @@ export default class PowerSelector extends React.Component { | |||
|             options, | ||||
|             custom: isCustom, | ||||
|             customLevel: newProps.value, | ||||
|             selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value, | ||||
|             selectValue: isCustom ? CUSTOM_VALUE : newProps.value, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     onSelectChange = event => { | ||||
|         const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; | ||||
|     private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => { | ||||
|         const isCustom = event.target.value === CUSTOM_VALUE; | ||||
|         if (isCustom) { | ||||
|             this.setState({ custom: true }); | ||||
|         } else { | ||||
|             this.props.onChange(event.target.value, this.props.powerLevelKey); | ||||
|             this.setState({ selectValue: event.target.value }); | ||||
|             const powerLevel = parseInt(event.target.value); | ||||
|             this.props.onChange(powerLevel, this.props.powerLevelKey); | ||||
|             this.setState({ selectValue: powerLevel }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onCustomChange = event => { | ||||
|         this.setState({ customValue: event.target.value }); | ||||
|     private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => { | ||||
|         this.setState({ customValue: parseInt(event.target.value) }); | ||||
|     }; | ||||
| 
 | ||||
|     onCustomBlur = event => { | ||||
|     private onCustomBlur = (event: React.FocusEvent): void => { | ||||
|         event.preventDefault(); | ||||
|         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) { | ||||
|             event.preventDefault(); | ||||
|             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
 | ||||
|             // so on. By not causing the onChange to be called here, we avoid the loop because we
 | ||||
|             // handle the onBlur safely.
 | ||||
|             event.target.blur(); | ||||
|             (event.target as HTMLInputElement).blur(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         let picker; | ||||
|         const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; | ||||
|         if (this.state.custom) { | ||||
|  | @ -147,14 +160,14 @@ export default class PowerSelector extends React.Component { | |||
|             ); | ||||
|         } else { | ||||
|             // 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 { | ||||
|                     value: level, | ||||
|                     value: String(level), | ||||
|                     text: Roles.textualPowerLevel(level, this.props.usersDefault), | ||||
|                 }; | ||||
|             }); | ||||
|             options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); | ||||
|             options = options.map((op) => { | ||||
|             options.push({ value: CUSTOM_VALUE, text: _t("Custom level") }); | ||||
|             const optionsElements = options.map((op) => { | ||||
|                 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)} | ||||
|                     disabled={this.props.disabled} | ||||
|                 > | ||||
|                     { options } | ||||
|                     { optionsElements } | ||||
|                 </Field> | ||||
|             ); | ||||
|         } | ||||
|  | @ -17,25 +17,34 @@ | |||
| import React from 'react'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     reason?: string; | ||||
|     contentHtml: string; | ||||
| } | ||||
| 
 | ||||
| interface IState { | ||||
|     visible: boolean; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.Spoiler") | ||||
| export default class Spoiler extends React.Component { | ||||
|     constructor(props) { | ||||
| export default class Spoiler extends React.Component<IProps, IState> { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
|         this.state = { | ||||
|             visible: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     toggleVisible(e) { | ||||
|     private toggleVisible = (e: React.MouseEvent): void => { | ||||
|         if (!this.state.visible) { | ||||
|             // we are un-blurring, we don't want this click to propagate to potential child pills
 | ||||
|             e.preventDefault(); | ||||
|             e.stopPropagation(); | ||||
|         } | ||||
|         this.setState({ visible: !this.state.visible }); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         const reason = this.props.reason ? ( | ||||
|             <span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span> | ||||
|         ) : 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
 | ||||
|         // HTML content. This is secure as the contents have already been parsed previously
 | ||||
|         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 } | ||||
|                   | ||||
|                 <span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} /> | ||||
|  | @ -15,40 +15,40 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { highlightBlock } from 'highlight.js'; | ||||
| import { replaceableComponent } from "../../../utils/replaceableComponent"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     className?: string; | ||||
|     children?: React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.SyntaxHighlight") | ||||
| export default class SyntaxHighlight extends React.Component { | ||||
|     static propTypes = { | ||||
|         className: PropTypes.string, | ||||
|         children: PropTypes.node, | ||||
|     }; | ||||
| export default class SyntaxHighlight extends React.Component<IProps> { | ||||
|     private el: HTMLPreElement = null; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this._ref = this._ref.bind(this); | ||||
|     } | ||||
| 
 | ||||
|     // componentDidUpdate used here for reusability
 | ||||
|     componentDidUpdate() { | ||||
|         if (this._el) highlightBlock(this._el); | ||||
|     public componentDidUpdate(): void { | ||||
|         if (this.el) highlightBlock(this.el); | ||||
|     } | ||||
| 
 | ||||
|     // call componentDidUpdate because _ref is fired on initial render
 | ||||
|     // which does not fire componentDidUpdate
 | ||||
|     _ref(el) { | ||||
|         this._el = el; | ||||
|     private ref = (el: HTMLPreElement): void => { | ||||
|         this.el = el; | ||||
|         this.componentDidUpdate(); | ||||
|     } | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|     public render(): JSX.Element { | ||||
|         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> | ||||
|         </pre>; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -15,42 +15,44 @@ | |||
|  */ | ||||
| 
 | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import * as sdk from '../../../index'; | ||||
| 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") | ||||
| export default class TextWithTooltip extends React.Component { | ||||
|     static propTypes = { | ||||
|         class: PropTypes.string, | ||||
|         tooltipClass: PropTypes.string, | ||||
|         tooltip: PropTypes.node.isRequired, | ||||
|         tooltipProps: PropTypes.object, | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super(); | ||||
| export default class TextWithTooltip extends React.Component<IProps, IState> { | ||||
|     constructor(props: IProps) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             hover: false, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     onMouseOver = () => { | ||||
|     private onMouseOver = (): void => { | ||||
|         this.setState({ hover: true }); | ||||
|     }; | ||||
| 
 | ||||
|     onMouseLeave = () => { | ||||
|     private onMouseLeave = (): void => { | ||||
|         this.setState({ hover: false }); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const Tooltip = sdk.getComponent("elements.Tooltip"); | ||||
| 
 | ||||
|     public render(): JSX.Element { | ||||
|         const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; | ||||
| 
 | ||||
|         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 } | ||||
|                 { this.state.hover && <Tooltip | ||||
|                     {...tooltipProps} | ||||
|  | @ -15,20 +15,20 @@ limitations under the License. | |||
| */ | ||||
| 
 | ||||
| import React from "react"; | ||||
| import PropTypes from "prop-types"; | ||||
| import { replaceableComponent } from "../../../../utils/replaceableComponent"; | ||||
| import QRCode from "../QRCode"; | ||||
| import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode"; | ||||
| 
 | ||||
| interface IProps { | ||||
|     qrCodeData: QRCodeData; | ||||
| } | ||||
| 
 | ||||
| @replaceableComponent("views.elements.crypto.VerificationQRCode") | ||||
| export default class VerificationQRCode extends React.PureComponent { | ||||
|     static propTypes = { | ||||
|         qrCodeData: PropTypes.object.isRequired, | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
| export default class VerificationQRCode extends React.PureComponent<IProps> { | ||||
|     public render(): JSX.Element { | ||||
|         return ( | ||||
|             <QRCode | ||||
|                 data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]} | ||||
|                 data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]} | ||||
|                 className="mx_VerificationQRCode" | ||||
|                 width={196} /> | ||||
|         ); | ||||
|  | @ -1052,8 +1052,7 @@ const PowerLevelEditor: React.FC<{ | |||
|     const cli = useContext(MatrixClientContext); | ||||
| 
 | ||||
|     const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); | ||||
|     const onPowerChange = useCallback(async (powerLevelStr: string) => { | ||||
|         const powerLevel = parseInt(powerLevelStr, 10); | ||||
|     const onPowerChange = useCallback(async (powerLevel: number) => { | ||||
|         setSelectedPowerLevel(powerLevel); | ||||
| 
 | ||||
|         const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { | ||||
|  |  | |||
|  | @ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => { | |||
|         <AppTile | ||||
|             app={app} | ||||
|             fullWidth | ||||
|             show | ||||
|             showMenubar={false} | ||||
|             room={room} | ||||
|             userId={cli.getUserId()} | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; | |||
| import { ActionPayload } from '../../../dispatcher/payloads'; | ||||
| import ScalarAuthClient from '../../../ScalarAuthClient'; | ||||
| 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).
 | ||||
| // 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"); | ||||
| 
 | ||||
|             // FIXME: could this use the same code as other apps?
 | ||||
|             const stickerApp = { | ||||
|             const stickerApp: IApp = { | ||||
|                 id: stickerpickerWidget.id, | ||||
|                 url: stickerpickerWidget.content.url, | ||||
|                 name: stickerpickerWidget.content.name, | ||||
|                 type: stickerpickerWidget.content.type, | ||||
|                 data: stickerpickerWidget.content.data, | ||||
|                 roomId: stickerpickerWidget.content.roomId, | ||||
|                 eventId: stickerpickerWidget.content.eventId, | ||||
|                 avatar_url: stickerpickerWidget.content.avatar_url, | ||||
|                 creatorUserId: stickerpickerWidget.content.creatorUserId, | ||||
|             }; | ||||
| 
 | ||||
|             stickersContent = ( | ||||
|  | @ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> { | |||
|                                 onEditClick={this.launchManageIntegrations} | ||||
|                                 onDeleteClick={this.removeStickerpickerWidgets} | ||||
|                                 showTitle={false} | ||||
|                                 showCancel={false} | ||||
|                                 showPopout={false} | ||||
|                                 onMinimiseClick={this.onHideStickersClick} | ||||
|                                 handleMinimisePointerEvents={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 | ||||
|      */ | ||||
|  |  | |||
|  | @ -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 room = client.getRoom(this.props.roomId); | ||||
|         const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); | ||||
|  | @ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> { | |||
| 
 | ||||
|         const eventsLevelPrefix = "event_levels_"; | ||||
| 
 | ||||
|         const value = parseInt(inputValue); | ||||
| 
 | ||||
|         if (powerLevelKey.startsWith(eventsLevelPrefix)) { | ||||
|             // deep copy "events" object, Object.assign itself won't deep copy
 | ||||
|             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 room = client.getRoom(this.props.roomId); | ||||
|         const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ''); | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ interface IAppTileProps { | |||
|     userId: string; | ||||
|     creatorUserId: string; | ||||
|     waitForIframeLoad: boolean; | ||||
|     whitelistCapabilities: string[]; | ||||
|     whitelistCapabilities?: string[]; | ||||
|     userWidget: boolean; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston