Merge pull request #6815 from SimonBrandner/task/elements-ts
Convert `/src/components/views/elements` to TS
This commit is contained in:
commit
2eea606442
23 changed files with 648 additions and 613 deletions
|
@ -26,10 +26,9 @@ import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
import Field from '../../../../components/views/elements/Field';
|
import Field from '../../../../components/views/elements/Field';
|
||||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
|
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends IDialogProps {}
|
||||||
onFinished: (confirmed: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
eventIndexSize: number;
|
eventIndexSize: number;
|
||||||
|
|
|
@ -76,7 +76,6 @@ const LeftPanelWidget: React.FC = () => {
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth
|
fullWidth
|
||||||
show
|
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
userWidget
|
userWidget
|
||||||
userId={cli.getUserId()}
|
userId={cli.getUserId()}
|
||||||
|
|
|
@ -23,10 +23,9 @@ import Modal from '../../../Modal';
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import QuestionDialog from "./QuestionDialog";
|
import QuestionDialog from "./QuestionDialog";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends IDialogProps {}
|
||||||
onFinished: (success: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
const CryptoStoreTooNewDialog: React.FC<IProps> = (props: IProps) => {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
@ -39,33 +38,95 @@ import { MatrixCapabilities } from "matrix-widget-api";
|
||||||
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||||
import WidgetAvatar from "../avatars/WidgetAvatar";
|
import WidgetAvatar from "../avatars/WidgetAvatar";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
app: IApp;
|
||||||
|
// If room is not specified then it is an account level widget
|
||||||
|
// which bypasses permission prompts as it was added explicitly by that user
|
||||||
|
room: Room;
|
||||||
|
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||||
|
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||||
|
fullWidth?: boolean;
|
||||||
|
// Optional. If set, renders a smaller view of the widget
|
||||||
|
miniMode?: boolean;
|
||||||
|
// UserId of the current user
|
||||||
|
userId: string;
|
||||||
|
// UserId of the entity that added / modified the widget
|
||||||
|
creatorUserId: string;
|
||||||
|
waitForIframeLoad: boolean;
|
||||||
|
showMenubar?: boolean;
|
||||||
|
// Optional onEditClickHandler (overrides default behaviour)
|
||||||
|
onEditClick?: () => void;
|
||||||
|
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||||
|
onDeleteClick?: () => void;
|
||||||
|
// Optionally hide the tile title
|
||||||
|
showTitle?: boolean;
|
||||||
|
// Optionally handle minimise button pointer events (default false)
|
||||||
|
handleMinimisePointerEvents?: boolean;
|
||||||
|
// Optionally hide the popout widget icon
|
||||||
|
showPopout?: boolean;
|
||||||
|
// Is this an instance of a user widget
|
||||||
|
userWidget: boolean;
|
||||||
|
// sets the pointer-events property on the iframe
|
||||||
|
pointerEvents?: string;
|
||||||
|
widgetPageTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
initialising: boolean; // True while we are mangling the widget URL
|
||||||
|
// True while the iframe content is loading
|
||||||
|
loading: boolean;
|
||||||
|
// Assume that widget has permission to load if we are the user who
|
||||||
|
// added it to the room, or if explicitly granted by the user
|
||||||
|
hasPermissionToLoad: boolean;
|
||||||
|
error: Error;
|
||||||
|
menuDisplayed: boolean;
|
||||||
|
widgetPageTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.AppTile")
|
@replaceableComponent("views.elements.AppTile")
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
waitForIframeLoad: true,
|
||||||
|
showMenubar: true,
|
||||||
|
showTitle: true,
|
||||||
|
showPopout: true,
|
||||||
|
handleMinimisePointerEvents: false,
|
||||||
|
userWidget: false,
|
||||||
|
miniMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private contextMenuButton = createRef<any>();
|
||||||
|
private iframe: HTMLIFrameElement; // ref to the iframe (callback style)
|
||||||
|
private allowedWidgetsWatchRef: string;
|
||||||
|
private persistKey: string;
|
||||||
|
private sgWidget: StopGapWidget;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// The key used for PersistedElement
|
||||||
this._persistKey = getPersistKey(this.props.app.id);
|
this.persistKey = getPersistKey(this.props.app.id);
|
||||||
try {
|
try {
|
||||||
this._sgWidget = new StopGapWidget(this.props);
|
this.sgWidget = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this.sgWidget.on("preparing", this.onWidgetPrepared);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this.sgWidget.on("ready", this.onWidgetReady);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to construct widget", e);
|
console.log("Failed to construct widget", e);
|
||||||
this._sgWidget = null;
|
this.sgWidget = null;
|
||||||
}
|
}
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
this.state = this.getNewState(props);
|
||||||
this._contextMenuButton = createRef();
|
|
||||||
|
|
||||||
this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a function to make the impact of calling SettingsStore slightly less
|
// This is a function to make the impact of calling SettingsStore slightly less
|
||||||
hasPermissionToLoad = (props) => {
|
private hasPermissionToLoad = (props: IProps): boolean => {
|
||||||
if (this._usingLocalWidget()) return true;
|
if (this.usingLocalWidget()) return true;
|
||||||
if (!props.room) return true; // user widgets always have permissions
|
if (!props.room) return true; // user widgets always have permissions
|
||||||
|
|
||||||
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
|
||||||
|
@ -81,34 +142,34 @@ export default class AppTile extends React.Component {
|
||||||
* @param {Object} newProps The new properties of the component
|
* @param {Object} newProps The new properties of the component
|
||||||
* @return {Object} Updated component state to be set with setState
|
* @return {Object} Updated component state to be set with setState
|
||||||
*/
|
*/
|
||||||
_getNewState(newProps) {
|
private getNewState(newProps: IProps): IState {
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
// True while the iframe content is loading
|
// True while the iframe content is loading
|
||||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this.persistKey),
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
// added it to the room, or if explicitly granted by the user
|
// added it to the room, or if explicitly granted by the user
|
||||||
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
hasPermissionToLoad: this.hasPermissionToLoad(newProps),
|
||||||
error: null,
|
error: null,
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
|
||||||
menuDisplayed: false,
|
menuDisplayed: false,
|
||||||
|
widgetPageTitle: this.props.widgetPageTitle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onAllowedWidgetsChange = () => {
|
private onAllowedWidgetsChange = (): void => {
|
||||||
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
const hasPermissionToLoad = this.hasPermissionToLoad(this.props);
|
||||||
|
|
||||||
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
|
||||||
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
// Force the widget to be non-persistent (able to be deleted/forgotten)
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
if (this._sgWidget) this._sgWidget.stop();
|
if (this.sgWidget) this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ hasPermissionToLoad });
|
this.setState({ hasPermissionToLoad });
|
||||||
};
|
};
|
||||||
|
|
||||||
isMixedContent() {
|
private isMixedContent(): boolean {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.app.url);
|
const u = url.parse(this.props.app.url);
|
||||||
const childContentProtocol = u.protocol;
|
const childContentProtocol = u.protocol;
|
||||||
|
@ -120,69 +181,70 @@ export default class AppTile extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// Only fetch IM token on mount if we're showing and have permission to load
|
||||||
if (this._sgWidget && this.state.hasPermissionToLoad) {
|
if (this.sgWidget && this.state.hasPermissionToLoad) {
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget action listeners
|
// Widget action listeners
|
||||||
this.dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
// Widget action listeners
|
// Widget action listeners
|
||||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._sgWidget) {
|
if (this.sgWidget) {
|
||||||
this._sgWidget.stop();
|
this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
|
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetWidget(newProps) {
|
private resetWidget(newProps: IProps): void {
|
||||||
if (this._sgWidget) {
|
if (this.sgWidget) {
|
||||||
this._sgWidget.stop();
|
this.sgWidget.stop();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this._sgWidget = new StopGapWidget(newProps);
|
this.sgWidget = new StopGapWidget(newProps);
|
||||||
this._sgWidget.on("preparing", this._onWidgetPrepared);
|
this.sgWidget.on("preparing", this.onWidgetPrepared);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this.sgWidget.on("ready", this.onWidgetReady);
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to construct widget", e);
|
console.log("Failed to construct widget", e);
|
||||||
this._sgWidget = null;
|
this.sgWidget = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_startWidget() {
|
private startWidget(): void {
|
||||||
this._sgWidget.prepare().then(() => {
|
this.sgWidget.prepare().then(() => {
|
||||||
this.setState({ initialising: false });
|
this.setState({ initialising: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_iframeRefChange = (ref) => {
|
private iframeRefChange = (ref: HTMLIFrameElement): void => {
|
||||||
this.iframe = ref;
|
this.iframe = ref;
|
||||||
if (ref) {
|
if (ref) {
|
||||||
if (this._sgWidget) this._sgWidget.start(ref);
|
if (this.sgWidget) this.sgWidget.start(ref);
|
||||||
} else {
|
} else {
|
||||||
this._resetWidget(this.props);
|
this.resetWidget(this.props);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void { // eslint-disable-line camelcase
|
||||||
if (nextProps.app.url !== this.props.app.url) {
|
if (nextProps.app.url !== this.props.app.url) {
|
||||||
this._getNewState(nextProps);
|
this.getNewState(nextProps);
|
||||||
if (this.state.hasPermissionToLoad) {
|
if (this.state.hasPermissionToLoad) {
|
||||||
this._resetWidget(nextProps);
|
this.resetWidget(nextProps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +260,7 @@ export default class AppTile extends React.Component {
|
||||||
* @private
|
* @private
|
||||||
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
* @returns {Promise<*>} Resolves when the widget is terminated, or timeout passed.
|
||||||
*/
|
*/
|
||||||
async _endWidgetActions() { // widget migration dev note: async to maintain signature
|
private async endWidgetActions(): Promise<void> { // widget migration dev note: async to maintain signature
|
||||||
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
// HACK: This is a really dirty way to ensure that Jitsi cleans up
|
||||||
// its hold on the webcam. Without this, the widget holds a media
|
// its hold on the webcam. Without this, the widget holds a media
|
||||||
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
// stream open, even after death. See https://github.com/vector-im/element-web/issues/7351
|
||||||
|
@ -217,27 +279,27 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the widget from the persisted store for good measure.
|
// Delete the widget from the persisted store for good measure.
|
||||||
PersistedElement.destroyElement(this._persistKey);
|
PersistedElement.destroyElement(this.persistKey);
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
|
||||||
|
|
||||||
if (this._sgWidget) this._sgWidget.stop({ forceDestroy: true });
|
if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onWidgetPrepared = () => {
|
private onWidgetPrepared = (): void => {
|
||||||
this.setState({ loading: false });
|
this.setState({ loading: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onWidgetReady = () => {
|
private onWidgetReady = (): void => {
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
this._sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
this.sgWidget.widgetApi.transport.send(ElementWidgetActions.ClientReady, {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onAction = payload => {
|
private onAction = (payload): void => {
|
||||||
if (payload.widgetId === this.props.app.id) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
if (this.sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||||
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
dis.dispatch({ action: 'post_sticker_message', data: payload.data });
|
||||||
dis.dispatch({ action: 'stickerpicker_close' });
|
dis.dispatch({ action: 'stickerpicker_close' });
|
||||||
} else {
|
} else {
|
||||||
|
@ -248,7 +310,7 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_grantWidgetPermission = () => {
|
private grantWidgetPermission = (): void => {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
|
@ -258,14 +320,14 @@ export default class AppTile extends React.Component {
|
||||||
this.setState({ hasPermissionToLoad: true });
|
this.setState({ hasPermissionToLoad: true });
|
||||||
|
|
||||||
// Fetch a token for the integration manager, now that we're allowed to
|
// Fetch a token for the integration manager, now that we're allowed to
|
||||||
this._startWidget();
|
this.startWidget();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// We don't really need to do anything about this - the user will just hit the button again.
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
formatAppTileName() {
|
private formatAppTileName(): string {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
if (this.props.app.name && this.props.app.name.trim()) {
|
if (this.props.app.name && this.props.app.name.trim()) {
|
||||||
appTileName = this.props.app.name.trim();
|
appTileName = this.props.app.name.trim();
|
||||||
|
@ -278,11 +340,11 @@ export default class AppTile extends React.Component {
|
||||||
* actual widget URL
|
* actual widget URL
|
||||||
* @returns {bool} true If using a local version of the widget
|
* @returns {bool} true If using a local version of the widget
|
||||||
*/
|
*/
|
||||||
_usingLocalWidget() {
|
private usingLocalWidget(): boolean {
|
||||||
return WidgetType.JITSI.matches(this.props.app.type);
|
return WidgetType.JITSI.matches(this.props.app.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTileTitle() {
|
private getTileTitle(): JSX.Element {
|
||||||
const name = this.formatAppTileName();
|
const name = this.formatAppTileName();
|
||||||
const titleSpacer = <span> - </span>;
|
const titleSpacer = <span> - </span>;
|
||||||
let title = '';
|
let title = '';
|
||||||
|
@ -300,32 +362,32 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO replace with full screen interactions
|
// TODO replace with full screen interactions
|
||||||
_onPopoutWidgetClick = () => {
|
private onPopoutWidgetClick = (): void => {
|
||||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
||||||
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
// twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop).
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
||||||
this._endWidgetActions().then(() => {
|
this.endWidgetActions().then(() => {
|
||||||
if (this.iframe) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
this.iframe.src = this._sgWidget.embedUrl;
|
this.iframe.src = this.sgWidget.embedUrl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
{ target: '_blank', href: this.sgWidget.popoutUrl, rel: 'noreferrer noopener' }).click();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onContextMenuClick = () => {
|
private onContextMenuClick = (): void => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_closeContextMenu = () => {
|
private closeContextMenu = (): void => {
|
||||||
this.setState({ menuDisplayed: false });
|
this.setState({ menuDisplayed: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
|
||||||
|
@ -351,7 +413,7 @@ export default class AppTile extends React.Component {
|
||||||
<Spinner message={_t("Loading...")} />
|
<Spinner message={_t("Loading...")} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (this._sgWidget === null) {
|
if (this.sgWidget === null) {
|
||||||
appTileBody = (
|
appTileBody = (
|
||||||
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
<div className={appTileBodyClass} style={appTileBodyStyles}>
|
||||||
<AppWarning errorMsg={_t("Error loading Widget")} />
|
<AppWarning errorMsg={_t("Error loading Widget")} />
|
||||||
|
@ -365,9 +427,9 @@ export default class AppTile extends React.Component {
|
||||||
<AppPermission
|
<AppPermission
|
||||||
roomId={this.props.room.roomId}
|
roomId={this.props.room.roomId}
|
||||||
creatorUserId={this.props.creatorUserId}
|
creatorUserId={this.props.creatorUserId}
|
||||||
url={this._sgWidget.embedUrl}
|
url={this.sgWidget.embedUrl}
|
||||||
isRoomEncrypted={isEncrypted}
|
isRoomEncrypted={isEncrypted}
|
||||||
onPermissionGranted={this._grantWidgetPermission}
|
onPermissionGranted={this.grantWidgetPermission}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -390,8 +452,8 @@ export default class AppTile extends React.Component {
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
allow={iframeFeatures}
|
allow={iframeFeatures}
|
||||||
ref={this._iframeRefChange}
|
ref={this.iframeRefChange}
|
||||||
src={this._sgWidget.embedUrl}
|
src={this.sgWidget.embedUrl}
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
/>
|
/>
|
||||||
|
@ -407,7 +469,7 @@ export default class AppTile extends React.Component {
|
||||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||||
// AppTile's border is in the wrong place
|
// AppTile's border is in the wrong place
|
||||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||||
<PersistedElement persistKey={this._persistKey}>
|
<PersistedElement persistKey={this.persistKey}>
|
||||||
{ appTileBody }
|
{ appTileBody }
|
||||||
</PersistedElement>
|
</PersistedElement>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -429,9 +491,9 @@ export default class AppTile extends React.Component {
|
||||||
if (this.state.menuDisplayed) {
|
if (this.state.menuDisplayed) {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<RoomWidgetContextMenu
|
<RoomWidgetContextMenu
|
||||||
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
|
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect(), null)}
|
||||||
app={this.props.app}
|
app={this.props.app}
|
||||||
onFinished={this._closeContextMenu}
|
onFinished={this.closeContextMenu}
|
||||||
showUnpin={!this.props.userWidget}
|
showUnpin={!this.props.userWidget}
|
||||||
userWidget={this.props.userWidget}
|
userWidget={this.props.userWidget}
|
||||||
onEditClick={this.props.onEditClick}
|
onEditClick={this.props.onEditClick}
|
||||||
|
@ -444,21 +506,21 @@ export default class AppTile extends React.Component {
|
||||||
<div className={appTileClasses} id={this.props.app.id}>
|
<div className={appTileClasses} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ this.props.showMenubar &&
|
||||||
<div className="mx_AppTileMenuBar">
|
<div className="mx_AppTileMenuBar">
|
||||||
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false) }}>
|
<span className="mx_AppTileMenuBarTitle" style={{ pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : "none") }}>
|
||||||
{ this.props.showTitle && this._getTileTitle() }
|
{ this.props.showTitle && this.getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ this.props.showPopout && <AccessibleButton
|
{ this.props.showPopout && <AccessibleButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||||
title={_t('Popout widget')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onPopoutWidgetClick}
|
onClick={this.onPopoutWidgetClick}
|
||||||
/> }
|
/> }
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||||
label={_t("Options")}
|
label={_t("Options")}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
inputRef={this._contextMenuButton}
|
inputRef={this.contextMenuButton}
|
||||||
onClick={this._onContextMenuClick}
|
onClick={this.onContextMenuClick}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div> }
|
</div> }
|
||||||
|
@ -469,49 +531,3 @@ export default class AppTile extends React.Component {
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AppTile.displayName = 'AppTile';
|
|
||||||
|
|
||||||
AppTile.propTypes = {
|
|
||||||
app: PropTypes.object.isRequired,
|
|
||||||
// If room is not specified then it is an account level widget
|
|
||||||
// which bypasses permission prompts as it was added explicitly by that user
|
|
||||||
room: PropTypes.object,
|
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
|
||||||
fullWidth: PropTypes.bool,
|
|
||||||
// Optional. If set, renders a smaller view of the widget
|
|
||||||
miniMode: PropTypes.bool,
|
|
||||||
// UserId of the current user
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
// UserId of the entity that added / modified the widget
|
|
||||||
creatorUserId: PropTypes.string,
|
|
||||||
waitForIframeLoad: PropTypes.bool,
|
|
||||||
showMenubar: PropTypes.bool,
|
|
||||||
// Optional onEditClickHandler (overrides default behaviour)
|
|
||||||
onEditClick: PropTypes.func,
|
|
||||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
|
||||||
onDeleteClick: PropTypes.func,
|
|
||||||
// Optional onMinimiseClickHandler
|
|
||||||
onMinimiseClick: PropTypes.func,
|
|
||||||
// Optionally hide the tile title
|
|
||||||
showTitle: PropTypes.bool,
|
|
||||||
// Optionally handle minimise button pointer events (default false)
|
|
||||||
handleMinimisePointerEvents: PropTypes.bool,
|
|
||||||
// Optionally hide the popout widget icon
|
|
||||||
showPopout: PropTypes.bool,
|
|
||||||
// Is this an instance of a user widget
|
|
||||||
userWidget: PropTypes.bool,
|
|
||||||
// sets the pointer-events property on the iframe
|
|
||||||
pointerEvents: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
AppTile.defaultProps = {
|
|
||||||
waitForIframeLoad: true,
|
|
||||||
showMenubar: true,
|
|
||||||
showTitle: true,
|
|
||||||
showPopout: true,
|
|
||||||
handleMinimisePointerEvents: false,
|
|
||||||
userWidget: false,
|
|
||||||
miniMode: false,
|
|
||||||
};
|
|
|
@ -1,24 +1,20 @@
|
||||||
import React from 'react'; // eslint-disable-line no-unused-vars
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const AppWarning = (props) => {
|
interface IProps {
|
||||||
|
errorMsg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppWarning: React.FC<IProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className='mx_AppPermissionWarning'>
|
<div className='mx_AppPermissionWarning'>
|
||||||
<div className='mx_AppPermissionWarningImage'>
|
<div className='mx_AppPermissionWarningImage'>
|
||||||
<img src={require("../../../../res/img/warning.svg")} alt='' />
|
<img src={require("../../../../res/img/warning.svg")} alt='' />
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_AppPermissionWarningText'>
|
<div className='mx_AppPermissionWarningText'>
|
||||||
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg }</span>
|
<span className='mx_AppPermissionWarningTextLabel'>{ props.errorMsg || "Error" }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AppWarning.propTypes = {
|
|
||||||
errorMsg: PropTypes.string,
|
|
||||||
};
|
|
||||||
AppWarning.defaultProps = {
|
|
||||||
errorMsg: 'Error',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppWarning;
|
export default AppWarning;
|
|
@ -17,60 +17,61 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// The primary button which is styled differently and has default focus.
|
||||||
|
primaryButton: React.ReactNode;
|
||||||
|
|
||||||
|
// A node to insert into the cancel button instead of default "Cancel"
|
||||||
|
cancelButton?: React.ReactNode;
|
||||||
|
|
||||||
|
// If true, make the primary button a form submit button (input type="submit")
|
||||||
|
primaryIsSubmit?: boolean;
|
||||||
|
|
||||||
|
// onClick handler for the primary button.
|
||||||
|
onPrimaryButtonClick?: (ev: React.MouseEvent) => void;
|
||||||
|
|
||||||
|
// should there be a cancel button? default: true
|
||||||
|
hasCancel?: boolean;
|
||||||
|
|
||||||
|
// The class of the cancel button, only used if a cancel button is
|
||||||
|
// enabled
|
||||||
|
cancelButtonClass?: string;
|
||||||
|
|
||||||
|
// onClick handler for the cancel button.
|
||||||
|
onCancel?: (...args: any[]) => void;
|
||||||
|
|
||||||
|
focus?: boolean;
|
||||||
|
|
||||||
|
// disables the primary and cancel buttons
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
// disables only the primary button
|
||||||
|
primaryDisabled?: boolean;
|
||||||
|
|
||||||
|
// something to stick next to the buttons, optionally
|
||||||
|
additive?: React.ReactNode;
|
||||||
|
|
||||||
|
primaryButtonClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic container for buttons in modal dialogs.
|
* Basic container for buttons in modal dialogs.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.DialogButtons")
|
@replaceableComponent("views.elements.DialogButtons")
|
||||||
export default class DialogButtons extends React.Component {
|
export default class DialogButtons extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public static defaultProps: Partial<IProps> = {
|
||||||
// The primary button which is styled differently and has default focus.
|
|
||||||
primaryButton: PropTypes.node.isRequired,
|
|
||||||
|
|
||||||
// A node to insert into the cancel button instead of default "Cancel"
|
|
||||||
cancelButton: PropTypes.node,
|
|
||||||
|
|
||||||
// If true, make the primary button a form submit button (input type="submit")
|
|
||||||
primaryIsSubmit: PropTypes.bool,
|
|
||||||
|
|
||||||
// onClick handler for the primary button.
|
|
||||||
onPrimaryButtonClick: PropTypes.func,
|
|
||||||
|
|
||||||
// should there be a cancel button? default: true
|
|
||||||
hasCancel: PropTypes.bool,
|
|
||||||
|
|
||||||
// The class of the cancel button, only used if a cancel button is
|
|
||||||
// enabled
|
|
||||||
cancelButtonClass: PropTypes.node,
|
|
||||||
|
|
||||||
// onClick handler for the cancel button.
|
|
||||||
onCancel: PropTypes.func,
|
|
||||||
|
|
||||||
focus: PropTypes.bool,
|
|
||||||
|
|
||||||
// disables the primary and cancel buttons
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
|
|
||||||
// disables only the primary button
|
|
||||||
primaryDisabled: PropTypes.bool,
|
|
||||||
|
|
||||||
// something to stick next to the buttons, optionally
|
|
||||||
additive: PropTypes.element,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
hasCancel: true,
|
hasCancel: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = (event: React.MouseEvent): void => {
|
||||||
this.props.onCancel();
|
this.props.onCancel(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let primaryButtonClassName = "mx_Dialog_primary";
|
let primaryButtonClassName = "mx_Dialog_primary";
|
||||||
if (this.props.primaryButtonClass) {
|
if (this.props.primaryButtonClass) {
|
||||||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||||
|
@ -82,7 +83,7 @@ export default class DialogButtons extends React.Component {
|
||||||
// important: the default type is 'submit' and this button comes before the
|
// important: the default type is 'submit' and this button comes before the
|
||||||
// primary in the DOM so will get form submissions unless we make it not a submit.
|
// primary in the DOM so will get form submissions unless we make it not a submit.
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this._onCancelClick}
|
onClick={this.onCancelClick}
|
||||||
className={this.props.cancelButtonClass}
|
className={this.props.cancelButtonClass}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
|
@ -14,71 +14,73 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ChangeEvent, createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import AccessibleButton from "./AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onClear?: () => void;
|
||||||
|
onJoinClick?: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
showJoinButton?: boolean;
|
||||||
|
initialText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.DirectorySearchBox")
|
@replaceableComponent("views.elements.DirectorySearchBox")
|
||||||
export default class DirectorySearchBox extends React.Component {
|
export default class DirectorySearchBox extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
private input = createRef<HTMLInputElement>();
|
||||||
super(props);
|
|
||||||
this._collectInput = this._collectInput.bind(this);
|
|
||||||
this._onClearClick = this._onClearClick.bind(this);
|
|
||||||
this._onChange = this._onChange.bind(this);
|
|
||||||
this._onKeyUp = this._onKeyUp.bind(this);
|
|
||||||
this._onJoinButtonClick = this._onJoinButtonClick.bind(this);
|
|
||||||
|
|
||||||
this.input = null;
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
value: this.props.initialText || '',
|
value: this.props.initialText || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectInput(e) {
|
private onClearClick = (): void => {
|
||||||
this.input = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClearClick() {
|
|
||||||
this.setState({ value: '' });
|
this.setState({ value: '' });
|
||||||
|
|
||||||
if (this.input) {
|
if (this.input.current) {
|
||||||
this.input.focus();
|
this.input.current.focus();
|
||||||
|
|
||||||
if (this.props.onClear) {
|
if (this.props.onClear) {
|
||||||
this.props.onClear();
|
this.props.onClear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onChange(ev) {
|
private onChange = (ev: ChangeEvent<HTMLInputElement>): void => {
|
||||||
if (!this.input) return;
|
if (!this.input.current) return;
|
||||||
this.setState({ value: ev.target.value });
|
this.setState({ value: ev.target.value });
|
||||||
|
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(ev.target.value);
|
this.props.onChange(ev.target.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onKeyUp(ev) {
|
private onKeyUp = (ev: React.KeyboardEvent): void => {
|
||||||
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
if (ev.key == 'Enter' && this.props.showJoinButton) {
|
||||||
if (this.props.onJoinClick) {
|
if (this.props.onJoinClick) {
|
||||||
this.props.onJoinClick(this.state.value);
|
this.props.onJoinClick(this.state.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onJoinButtonClick() {
|
private onJoinButtonClick = (): void => {
|
||||||
if (this.props.onJoinClick) {
|
if (this.props.onJoinClick) {
|
||||||
this.props.onJoinClick(this.state.value);
|
this.props.onJoinClick(this.state.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
const searchboxClasses = {
|
const searchboxClasses = {
|
||||||
mx_DirectorySearchBox: true,
|
mx_DirectorySearchBox: true,
|
||||||
};
|
};
|
||||||
|
@ -87,7 +89,7 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
let joinButton;
|
let joinButton;
|
||||||
if (this.props.showJoinButton) {
|
if (this.props.showJoinButton) {
|
||||||
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
joinButton = <AccessibleButton className="mx_DirectorySearchBox_joinButton"
|
||||||
onClick={this._onJoinButtonClick}
|
onClick={this.onJoinButtonClick}
|
||||||
>{ _t("Join") }</AccessibleButton>;
|
>{ _t("Join") }</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,24 +99,15 @@ export default class DirectorySearchBox extends React.Component {
|
||||||
name="dirsearch"
|
name="dirsearch"
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
ref={this._collectInput}
|
ref={this.input}
|
||||||
onChange={this._onChange}
|
onChange={this.onChange}
|
||||||
onKeyUp={this._onKeyUp}
|
onKeyUp={this.onKeyUp}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{ joinButton }
|
{ joinButton }
|
||||||
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
|
<AccessibleButton className="mx_DirectorySearchBox_clear" onClick={this.onClearClick} />
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DirectorySearchBox.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onClear: PropTypes.func,
|
|
||||||
onJoinClick: PropTypes.func,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
showJoinButton: PropTypes.bool,
|
|
||||||
initialText: PropTypes.string,
|
|
||||||
};
|
|
|
@ -16,33 +16,42 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.EditableText")
|
enum Phases {
|
||||||
export default class EditableText extends React.Component {
|
Display = "display",
|
||||||
static propTypes = {
|
Edit = "edit",
|
||||||
onValueChanged: PropTypes.func,
|
}
|
||||||
initialValue: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
interface IProps {
|
||||||
placeholder: PropTypes.string,
|
onValueChanged?: (value: string, shouldSubmit: boolean) => void;
|
||||||
className: PropTypes.string,
|
initialValue?: string;
|
||||||
labelClassName: PropTypes.string,
|
label?: string;
|
||||||
placeholderClassName: PropTypes.string,
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
placeholderClassName?: string;
|
||||||
// Overrides blurToSubmit if true
|
// Overrides blurToSubmit if true
|
||||||
blurToCancel: PropTypes.bool,
|
blurToCancel?: boolean;
|
||||||
// Will cause onValueChanged(value, true) to fire on blur
|
// Will cause onValueChanged(value, true) to fire on blur
|
||||||
blurToSubmit: PropTypes.bool,
|
blurToSubmit?: boolean;
|
||||||
editable: PropTypes.bool,
|
editable?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
static Phases = {
|
interface IState {
|
||||||
Display: "display",
|
phase: Phases;
|
||||||
Edit: "edit",
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
@replaceableComponent("views.elements.EditableText")
|
||||||
|
export default class EditableText extends React.Component<IProps, IState> {
|
||||||
|
// we track value as an JS object field rather than in React state
|
||||||
|
// as React doesn't play nice with contentEditable.
|
||||||
|
public value = '';
|
||||||
|
private placeholder = false;
|
||||||
|
private editableDiv = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
onValueChanged() {},
|
onValueChanged() {},
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
label: '',
|
label: '',
|
||||||
|
@ -53,81 +62,61 @@ export default class EditableText extends React.Component {
|
||||||
blurToSubmit: false,
|
blurToSubmit: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// we track value as an JS object field rather than in React state
|
this.state = {
|
||||||
// as React doesn't play nice with contentEditable.
|
phase: Phases.Display,
|
||||||
this.value = '';
|
|
||||||
this.placeholder = false;
|
|
||||||
|
|
||||||
this._editable_div = createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
phase: EditableText.Phases.Display,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line @typescript-eslint/naming-convention, camelcase
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
public UNSAFE_componentWillReceiveProps(nextProps: IProps): void {
|
||||||
if (nextProps.initialValue !== this.props.initialValue) {
|
if (nextProps.initialValue !== this.props.initialValue) {
|
||||||
this.value = nextProps.initialValue;
|
this.value = nextProps.initialValue;
|
||||||
if (this._editable_div.current) {
|
if (this.editableDiv.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
if (this._editable_div.current) {
|
if (this.editableDiv.current) {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showPlaceholder = show => {
|
private showPlaceholder = (show: boolean): void => {
|
||||||
if (show) {
|
if (show) {
|
||||||
this._editable_div.current.textContent = this.props.placeholder;
|
this.editableDiv.current.textContent = this.props.placeholder;
|
||||||
this._editable_div.current.setAttribute("class", this.props.className
|
this.editableDiv.current.setAttribute("class", this.props.className
|
||||||
+ " " + this.props.placeholderClassName);
|
+ " " + this.props.placeholderClassName);
|
||||||
this.placeholder = true;
|
this.placeholder = true;
|
||||||
this.value = '';
|
this.value = '';
|
||||||
} else {
|
} else {
|
||||||
this._editable_div.current.textContent = this.value;
|
this.editableDiv.current.textContent = this.value;
|
||||||
this._editable_div.current.setAttribute("class", this.props.className);
|
this.editableDiv.current.setAttribute("class", this.props.className);
|
||||||
this.placeholder = false;
|
this.placeholder = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getValue = () => this.value;
|
private cancelEdit = (): void => {
|
||||||
|
|
||||||
setValue = value => {
|
|
||||||
this.value = value;
|
|
||||||
this.showPlaceholder(!this.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
edit = () => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Edit,
|
phase: Phases.Display,
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
cancelEdit = () => {
|
|
||||||
this.setState({
|
|
||||||
phase: EditableText.Phases.Display,
|
|
||||||
});
|
});
|
||||||
this.value = this.props.initialValue;
|
this.value = this.props.initialValue;
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
this.onValueChanged(false);
|
this.onValueChanged(false);
|
||||||
this._editable_div.current.blur();
|
this.editableDiv.current.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
onValueChanged = shouldSubmit => {
|
private onValueChanged = (shouldSubmit: boolean): void => {
|
||||||
this.props.onValueChanged(this.value, shouldSubmit);
|
this.props.onValueChanged(this.value, shouldSubmit);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyDown = ev => {
|
private onKeyDown = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
if (this.placeholder) {
|
if (this.placeholder) {
|
||||||
|
@ -142,13 +131,13 @@ export default class EditableText extends React.Component {
|
||||||
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyUp = ev => {
|
private onKeyUp = (ev: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
if (!ev.target.textContent) {
|
if (!(ev.target as HTMLDivElement).textContent) {
|
||||||
this.showPlaceholder(true);
|
this.showPlaceholder(true);
|
||||||
} else if (!this.placeholder) {
|
} else if (!this.placeholder) {
|
||||||
this.value = ev.target.textContent;
|
this.value = (ev.target as HTMLDivElement).textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.key === Key.ENTER) {
|
if (ev.key === Key.ENTER) {
|
||||||
|
@ -160,22 +149,22 @@ export default class EditableText extends React.Component {
|
||||||
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickDiv = ev => {
|
private onClickDiv = (): void => {
|
||||||
if (!this.props.editable) return;
|
if (!this.props.editable) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Edit,
|
phase: Phases.Edit,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onFocus = ev => {
|
private onFocus = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||||
|
|
||||||
const node = ev.target.childNodes[0];
|
const node = ev.target.childNodes[0];
|
||||||
if (node) {
|
if (node) {
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.setStart(node, 0);
|
range.setStart(node, 0);
|
||||||
range.setEnd(node, node.length);
|
range.setEnd(node, ev.target.childNodes.length);
|
||||||
|
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
@ -183,11 +172,15 @@ export default class EditableText extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onFinish = (ev, shouldSubmit) => {
|
private onFinish = (
|
||||||
|
ev: React.KeyboardEvent<HTMLDivElement> | React.FocusEvent<HTMLDivElement>,
|
||||||
|
shouldSubmit?: boolean,
|
||||||
|
): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self = this;
|
const self = this;
|
||||||
const submit = (ev.key === Key.ENTER) || shouldSubmit;
|
const submit = ("key" in ev && ev.key === Key.ENTER) || shouldSubmit;
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: EditableText.Phases.Display,
|
phase: Phases.Display,
|
||||||
}, () => {
|
}, () => {
|
||||||
if (this.value !== this.props.initialValue) {
|
if (this.value !== this.props.initialValue) {
|
||||||
self.onValueChanged(submit);
|
self.onValueChanged(submit);
|
||||||
|
@ -195,7 +188,7 @@ export default class EditableText extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur = ev => {
|
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
sel.removeAllRanges();
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
@ -208,11 +201,11 @@ export default class EditableText extends React.Component {
|
||||||
this.showPlaceholder(!this.value);
|
this.showPlaceholder(!this.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { className, editable, initialValue, label, labelClassName } = this.props;
|
const { className, editable, initialValue, label, labelClassName } = this.props;
|
||||||
let editableEl;
|
let editableEl;
|
||||||
|
|
||||||
if (!editable || (this.state.phase === EditableText.Phases.Display &&
|
if (!editable || (this.state.phase === Phases.Display &&
|
||||||
(label || labelClassName) && !this.value)
|
(label || labelClassName) && !this.value)
|
||||||
) {
|
) {
|
||||||
// show the label
|
// show the label
|
||||||
|
@ -222,7 +215,7 @@ export default class EditableText extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
editableEl = <div
|
editableEl = <div
|
||||||
ref={this._editable_div}
|
ref={this.editableDiv}
|
||||||
contentEditable={true}
|
contentEditable={true}
|
||||||
className={className}
|
className={className}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
|
@ -15,9 +15,34 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import EditableText from "./EditableText";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
/* callback to retrieve the initial value. */
|
||||||
|
getInitialValue?: () => Promise<string>;
|
||||||
|
|
||||||
|
/* initial value; used if getInitialValue is not given */
|
||||||
|
initialValue?: string;
|
||||||
|
|
||||||
|
/* placeholder text to use when the value is empty (and not being
|
||||||
|
* edited) */
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
/* callback to update the value. Called with a single argument: the new
|
||||||
|
* value. */
|
||||||
|
onSubmit?: (value: string) => Promise<{} | void>;
|
||||||
|
|
||||||
|
/* should the input submit when focus is lost? */
|
||||||
|
blurToSubmit?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
errorString: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component which wraps an EditableText, with a spinner while updates take
|
* A component which wraps an EditableText, with a spinner while updates take
|
||||||
|
@ -31,50 +56,51 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
* taken from the 'initialValue' property.
|
* taken from the 'initialValue' property.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.EditableTextContainer")
|
@replaceableComponent("views.elements.EditableTextContainer")
|
||||||
export default class EditableTextContainer extends React.Component {
|
export default class EditableTextContainer extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
private unmounted = false;
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
initialValue: "",
|
||||||
|
placeholder: "",
|
||||||
|
blurToSubmit: false,
|
||||||
|
onSubmit: () => { return Promise.resolve(); },
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
errorString: null,
|
errorString: null,
|
||||||
value: props.initialValue,
|
value: props.initialValue,
|
||||||
};
|
};
|
||||||
this._onValueChanged = this._onValueChanged.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public async componentDidMount(): Promise<void> {
|
||||||
if (this.props.getInitialValue === undefined) {
|
|
||||||
// use whatever was given in the initialValue property.
|
// use whatever was given in the initialValue property.
|
||||||
return;
|
if (this.props.getInitialValue === undefined) return;
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
|
try {
|
||||||
this.props.getInitialValue().then(
|
const initialValue = await this.props.getInitialValue();
|
||||||
(result) => {
|
if (this.unmounted) return;
|
||||||
if (this._unmounted) { return; }
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
value: result,
|
value: initialValue,
|
||||||
});
|
});
|
||||||
},
|
} catch (error) {
|
||||||
(error) => {
|
if (this.unmounted) return;
|
||||||
if (this._unmounted) { return; }
|
|
||||||
this.setState({
|
this.setState({
|
||||||
errorString: error.toString(),
|
errorString: error.toString(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onValueChanged(value, shouldSubmit) {
|
private onValueChanged = (value: string, shouldSubmit: boolean): void => {
|
||||||
if (!shouldSubmit) {
|
if (!shouldSubmit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -86,38 +112,36 @@ export default class EditableTextContainer extends React.Component {
|
||||||
|
|
||||||
this.props.onSubmit(value).then(
|
this.props.onSubmit(value).then(
|
||||||
() => {
|
() => {
|
||||||
if (this._unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
value: value,
|
value: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (this._unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
this.setState({
|
this.setState({
|
||||||
errorString: error.toString(),
|
errorString: error.toString(),
|
||||||
busy: false,
|
busy: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.busy) {
|
if (this.state.busy) {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
return (
|
return (
|
||||||
<Loader />
|
<Spinner />
|
||||||
);
|
);
|
||||||
} else if (this.state.errorString) {
|
} else if (this.state.errorString) {
|
||||||
return (
|
return (
|
||||||
<div className="error">{ this.state.errorString }</div>
|
<div className="error">{ this.state.errorString }</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const EditableText = sdk.getComponent('elements.EditableText');
|
|
||||||
return (
|
return (
|
||||||
<EditableText initialValue={this.state.value}
|
<EditableText initialValue={this.state.value}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={this.props.placeholder}
|
||||||
onValueChanged={this._onValueChanged}
|
onValueChanged={this.onValueChanged}
|
||||||
blurToSubmit={this.props.blurToSubmit}
|
blurToSubmit={this.props.blurToSubmit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -125,28 +149,3 @@ export default class EditableTextContainer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditableTextContainer.propTypes = {
|
|
||||||
/* callback to retrieve the initial value. */
|
|
||||||
getInitialValue: PropTypes.func,
|
|
||||||
|
|
||||||
/* initial value; used if getInitialValue is not given */
|
|
||||||
initialValue: PropTypes.string,
|
|
||||||
|
|
||||||
/* placeholder text to use when the value is empty (and not being
|
|
||||||
* edited) */
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
|
|
||||||
/* callback to update the value. Called with a single argument: the new
|
|
||||||
* value. */
|
|
||||||
onSubmit: PropTypes.func,
|
|
||||||
|
|
||||||
/* should the input submit when focus is lost? */
|
|
||||||
blurToSubmit: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
EditableTextContainer.defaultProps = {
|
|
||||||
initialValue: "",
|
|
||||||
placeholder: "",
|
|
||||||
blurToSubmit: false,
|
|
||||||
onSubmit: function(v) {return Promise.resolve(); },
|
|
||||||
};
|
|
|
@ -16,13 +16,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import * as languageHandler from '../../../languageHandler';
|
import * as languageHandler from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import Dropdown from "./Dropdown";
|
||||||
|
|
||||||
function languageMatchesSearchQuery(query, language) {
|
function languageMatchesSearchQuery(query, language) {
|
||||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||||
|
@ -30,11 +30,22 @@ function languageMatchesSearchQuery(query, language) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
onOptionChange: (language: string) => void;
|
||||||
|
value?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
searchQuery: string;
|
||||||
|
langs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.LanguageDropdown")
|
@replaceableComponent("views.elements.LanguageDropdown")
|
||||||
export default class LanguageDropdown extends React.Component {
|
export default class LanguageDropdown extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._onSearchChange = this._onSearchChange.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
|
@ -42,7 +53,7 @@ export default class LanguageDropdown extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
languageHandler.getAllLanguagesFromJson().then((langs) => {
|
||||||
langs.sort(function(a, b) {
|
langs.sort(function(a, b) {
|
||||||
if (a.label < b.label) return -1;
|
if (a.label < b.label) return -1;
|
||||||
|
@ -63,20 +74,17 @@ export default class LanguageDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSearchChange(search) {
|
private onSearchChange = (search: string): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery: search,
|
searchQuery: search,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.langs === null) {
|
if (this.state.langs === null) {
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
|
||||||
|
|
||||||
let displayedLanguages;
|
let displayedLanguages;
|
||||||
if (this.state.searchQuery) {
|
if (this.state.searchQuery) {
|
||||||
displayedLanguages = this.state.langs.filter((lang) => {
|
displayedLanguages = this.state.langs.filter((lang) => {
|
||||||
|
@ -107,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
|
||||||
id="mx_LanguageDropdown"
|
id="mx_LanguageDropdown"
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
onOptionChange={this.props.onOptionChange}
|
onOptionChange={this.props.onOptionChange}
|
||||||
onSearchChange={this._onSearchChange}
|
onSearchChange={this.onSearchChange}
|
||||||
searchEnabled={true}
|
searchEnabled={true}
|
||||||
value={value}
|
value={value}
|
||||||
label={_t("Language Dropdown")}
|
label={_t("Language Dropdown")}
|
||||||
|
@ -118,8 +126,3 @@ export default class LanguageDropdown extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LanguageDropdown.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
onOptionChange: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
|
@ -15,17 +15,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
class ItemRange {
|
class ItemRange {
|
||||||
constructor(topCount, renderCount, bottomCount) {
|
constructor(
|
||||||
this.topCount = topCount;
|
public topCount: number,
|
||||||
this.renderCount = renderCount;
|
public renderCount: number,
|
||||||
this.bottomCount = bottomCount;
|
public bottomCount: number,
|
||||||
}
|
) { }
|
||||||
|
|
||||||
contains(range) {
|
public contains(range: ItemRange): boolean {
|
||||||
// don't contain empty ranges
|
// don't contain empty ranges
|
||||||
// as it will prevent clearing the list
|
// as it will prevent clearing the list
|
||||||
// once it is scrolled far enough out of view
|
// once it is scrolled far enough out of view
|
||||||
|
@ -36,7 +35,7 @@ class ItemRange {
|
||||||
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
expand(amount) {
|
public expand(amount: number): ItemRange {
|
||||||
// don't expand ranges that won't render anything
|
// don't expand ranges that won't render anything
|
||||||
if (this.renderCount === 0) {
|
if (this.renderCount === 0) {
|
||||||
return this;
|
return this;
|
||||||
|
@ -51,20 +50,55 @@ class ItemRange {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalSize() {
|
public totalSize(): number {
|
||||||
return this.topCount + this.renderCount + this.bottomCount;
|
return this.topCount + this.renderCount + this.bottomCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
@replaceableComponent("views.elements.LazyRenderList")
|
||||||
export default class LazyRenderList extends React.Component {
|
export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> {
|
||||||
constructor(props) {
|
public static defaultProps: Partial<IProps<unknown>> = {
|
||||||
|
overflowItems: 20,
|
||||||
|
overflowMargin: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: IProps<T>) {
|
||||||
super(props);
|
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 range = LazyRenderList.getVisibleRangeFromProps(props);
|
||||||
const intersectRange = range.expand(props.overflowMargin);
|
const intersectRange = range.expand(props.overflowMargin);
|
||||||
const renderRange = range.expand(props.overflowItems);
|
const renderRange = range.expand(props.overflowItems);
|
||||||
|
@ -77,7 +111,7 @@ export default class LazyRenderList extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getVisibleRangeFromProps(props) {
|
private static getVisibleRangeFromProps(props: IProps<unknown>): ItemRange {
|
||||||
const { items, itemHeight, scrollTop, height } = props;
|
const { items, itemHeight, scrollTop, height } = props;
|
||||||
const length = items ? items.length : 0;
|
const length = items ? items.length : 0;
|
||||||
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
|
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
|
||||||
|
@ -88,7 +122,7 @@ export default class LazyRenderList extends React.Component {
|
||||||
return new ItemRange(topCount, renderCount, bottomCount);
|
return new ItemRange(topCount, renderCount, bottomCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { itemHeight, items, renderItem } = this.props;
|
const { itemHeight, items, renderItem } = this.props;
|
||||||
const { renderRange } = this.state;
|
const { renderRange } = this.state;
|
||||||
const { topCount, renderCount, bottomCount } = renderRange;
|
const { topCount, renderCount, bottomCount } = renderRange;
|
||||||
|
@ -109,28 +143,3 @@ export default class LazyRenderList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LazyRenderList.defaultProps = {
|
|
||||||
overflowItems: 20,
|
|
||||||
overflowMargin: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
LazyRenderList.propTypes = {
|
|
||||||
// height in pixels of the component returned by `renderItem`
|
|
||||||
itemHeight: PropTypes.number.isRequired,
|
|
||||||
// function to turn an element of `items` into a react component
|
|
||||||
renderItem: PropTypes.func.isRequired,
|
|
||||||
// scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
// the height of the viewport this content is scrolled in
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
// all items for the list. These should not be react components, see `renderItem`.
|
|
||||||
items: PropTypes.array,
|
|
||||||
// the amount of items to scroll before causing a rerender,
|
|
||||||
// should typically be less than `overflowItems` unless applying
|
|
||||||
// margins in the parent component when using multiple LazyRenderList in one viewport.
|
|
||||||
// use 0 to only rerender when items will come into view.
|
|
||||||
overflowMargin: PropTypes.number,
|
|
||||||
// the amount of items to add at the top and bottom to render,
|
|
||||||
// so not every scroll of causes a rerender.
|
|
||||||
overflowItems: PropTypes.number,
|
|
||||||
};
|
|
|
@ -16,25 +16,26 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
|
||||||
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
|
|
||||||
|
export const getPersistKey = (appId: string) => 'widget_' + appId;
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
// pass in a custom control as the actual body.
|
// pass in a custom control as the actual body.
|
||||||
|
|
||||||
function getContainer(containerId) {
|
function getContainer(containerId: string): HTMLDivElement {
|
||||||
return document.getElementById(containerId);
|
return document.getElementById(containerId) as HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateContainer(containerId) {
|
function getOrCreateContainer(containerId: string): HTMLDivElement {
|
||||||
let container = getContainer(containerId);
|
let container = getContainer(containerId);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
@ -46,7 +47,19 @@ function getOrCreateContainer(containerId) {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
|
// Unique identifier for this PersistedElement instance
|
||||||
|
// Any PersistedElements with the same persistKey will use
|
||||||
|
// the same DOM container.
|
||||||
|
persistKey: string;
|
||||||
|
|
||||||
|
// z-index for the element. Defaults to 9.
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
style?: React.StyleHTMLAttributes<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||||
* in a container element appended to document.body.
|
* in a container element appended to document.body.
|
||||||
*
|
*
|
||||||
|
@ -58,42 +71,33 @@ function getOrCreateContainer(containerId) {
|
||||||
* bounding rect as the parent of PE.
|
* bounding rect as the parent of PE.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.elements.PersistedElement")
|
@replaceableComponent("views.elements.PersistedElement")
|
||||||
export default class PersistedElement extends React.Component {
|
export default class PersistedElement extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private resizeObserver: ResizeObserver;
|
||||||
// Unique identifier for this PersistedElement instance
|
private dispatcherRef: string;
|
||||||
// Any PersistedElements with the same persistKey will use
|
private childContainer: HTMLDivElement;
|
||||||
// the same DOM container.
|
private child: HTMLDivElement;
|
||||||
persistKey: PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
// z-index for the element. Defaults to 9.
|
constructor(props: IProps) {
|
||||||
zIndex: PropTypes.number,
|
super(props);
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
this.resizeObserver = new ResizeObserver(this.repositionChild);
|
||||||
super();
|
|
||||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
|
||||||
this.collectChild = this.collectChild.bind(this);
|
|
||||||
this._repositionChild = this._repositionChild.bind(this);
|
|
||||||
this._onAction = this._onAction.bind(this);
|
|
||||||
|
|
||||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
|
||||||
// Annoyingly, a resize observer is insufficient, since we also care
|
// Annoyingly, a resize observer is insufficient, since we also care
|
||||||
// about when the element moves on the screen without changing its
|
// about when the element moves on the screen without changing its
|
||||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||||
// for this, so we bodge it by listening for document resize and
|
// for this, so we bodge it by listening for document resize and
|
||||||
// the timeline_resize action.
|
// the timeline_resize action.
|
||||||
window.addEventListener('resize', this._repositionChild);
|
window.addEventListener('resize', this.repositionChild);
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the DOM elements created when a PersistedElement with the given
|
* Removes the DOM elements created when a PersistedElement with the given
|
||||||
* persistKey was mounted. The DOM elements will be re-added if another
|
* persistKey was mounted. The DOM elements will be re-added if another
|
||||||
* PeristedElement is mounted in the future.
|
* PersistedElement is mounted in the future.
|
||||||
*
|
*
|
||||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||||
*/
|
*/
|
||||||
static destroyElement(persistKey) {
|
public static destroyElement(persistKey: string): void {
|
||||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.remove();
|
container.remove();
|
||||||
|
@ -104,7 +108,7 @@ export default class PersistedElement extends React.Component {
|
||||||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
collectChildContainer(ref) {
|
private collectChildContainer = (ref: HTMLDivElement): void => {
|
||||||
if (this.childContainer) {
|
if (this.childContainer) {
|
||||||
this.resizeObserver.unobserve(this.childContainer);
|
this.resizeObserver.unobserve(this.childContainer);
|
||||||
}
|
}
|
||||||
|
@ -112,48 +116,48 @@ export default class PersistedElement extends React.Component {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
this.resizeObserver.observe(ref);
|
this.resizeObserver.observe(ref);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
collectChild(ref) {
|
private collectChild = (ref: HTMLDivElement): void => {
|
||||||
this.child = ref;
|
this.child = ref;
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
}
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
this.renderApp();
|
this.renderApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
public componentDidUpdate(): void {
|
||||||
this.updateChild();
|
this.updateChild();
|
||||||
this.renderApp();
|
this.renderApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount(): void {
|
||||||
this.updateChildVisibility(this.child, false);
|
this.updateChildVisibility(this.child, false);
|
||||||
this.resizeObserver.disconnect();
|
this.resizeObserver.disconnect();
|
||||||
window.removeEventListener('resize', this._repositionChild);
|
window.removeEventListener('resize', this.repositionChild);
|
||||||
dis.unregister(this._dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction(payload) {
|
private onAction = (payload: ActionPayload): void => {
|
||||||
if (payload.action === 'timeline_resize') {
|
if (payload.action === 'timeline_resize') {
|
||||||
this._repositionChild();
|
this.repositionChild();
|
||||||
} else if (payload.action === 'logout') {
|
} else if (payload.action === 'logout') {
|
||||||
PersistedElement.destroyElement(this.props.persistKey);
|
PersistedElement.destroyElement(this.props.persistKey);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_repositionChild() {
|
private repositionChild = (): void => {
|
||||||
this.updateChildPosition(this.child, this.childContainer);
|
this.updateChildPosition(this.child, this.childContainer);
|
||||||
}
|
};
|
||||||
|
|
||||||
updateChild() {
|
private updateChild(): void {
|
||||||
this.updateChildPosition(this.child, this.childContainer);
|
this.updateChildPosition(this.child, this.childContainer);
|
||||||
this.updateChildVisibility(this.child, true);
|
this.updateChildVisibility(this.child, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderApp() {
|
private renderApp(): void {
|
||||||
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
const content = <MatrixClientContext.Provider value={MatrixClientPeg.get()}>
|
||||||
<div ref={this.collectChild} style={this.props.style}>
|
<div ref={this.collectChild} style={this.props.style}>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
|
@ -163,12 +167,12 @@ export default class PersistedElement extends React.Component {
|
||||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildVisibility(child, visible) {
|
private updateChildVisibility(child: HTMLDivElement, visible: boolean): void {
|
||||||
if (!child) return;
|
if (!child) return;
|
||||||
child.style.display = visible ? 'block' : 'none';
|
child.style.display = visible ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildPosition = throttle((child, parent) => {
|
private updateChildPosition = throttle((child: HTMLDivElement, parent: HTMLDivElement): void => {
|
||||||
if (!child || !parent) return;
|
if (!child || !parent) return;
|
||||||
|
|
||||||
const parentRect = parent.getBoundingClientRect();
|
const parentRect = parent.getBoundingClientRect();
|
||||||
|
@ -182,9 +186,8 @@ export default class PersistedElement extends React.Component {
|
||||||
});
|
});
|
||||||
}, 100, { trailing: true, leading: true });
|
}, 100, { trailing: true, leading: true });
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPersistKey = (appId) => 'widget_' + appId;
|
|
|
@ -19,57 +19,70 @@ import React from 'react';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { EventSubscription } from 'fbemitter';
|
||||||
|
import AppTile from "./AppTile";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
roomId: string;
|
||||||
|
persistentWidgetId: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PersistentApp")
|
@replaceableComponent("views.elements.PersistentApp")
|
||||||
export default class PersistentApp extends React.Component {
|
export default class PersistentApp extends React.Component<{}, IState> {
|
||||||
state = {
|
private roomStoreToken: EventSubscription;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
|
||||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentDidMount(): void {
|
||||||
if (this._roomStoreToken) {
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this._roomStoreToken.remove();
|
ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate);
|
||||||
|
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
if (this.roomStoreToken) {
|
||||||
|
this.roomStoreToken.remove();
|
||||||
|
}
|
||||||
|
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
|
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate = payload => {
|
private onRoomViewStoreUpdate = (): void => {
|
||||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActiveWidgetStoreUpdate = () => {
|
private onActiveWidgetStoreUpdate = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMyMembership = async (room, membership) => {
|
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
if (membership !== "join") {
|
if (membership !== "join") {
|
||||||
// we're not in the room anymore - delete
|
// we're not in the room anymore - delete
|
||||||
if (room.roomId === persistentWidgetInRoomId) {
|
if (room .roomId === persistentWidgetInRoomId) {
|
||||||
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
|
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
if (this.state.persistentWidgetId) {
|
if (this.state.persistentWidgetId) {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||||
|
|
||||||
|
@ -89,7 +102,6 @@ export default class PersistentApp extends React.Component {
|
||||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||||
persistentWidgetInRoomId, appEvent.getId(),
|
persistentWidgetInRoomId, appEvent.getId(),
|
||||||
);
|
);
|
||||||
const AppTile = sdk.getComponent('elements.AppTile');
|
|
||||||
return <AppTile
|
return <AppTile
|
||||||
key={app.id}
|
key={app.id}
|
||||||
app={app}
|
app={app}
|
|
@ -15,40 +15,52 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as Roles from '../../../Roles';
|
import * as Roles from '../../../Roles';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "./Field";
|
import Field from "./Field";
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PowerSelector")
|
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
|
||||||
export default class PowerSelector extends React.Component {
|
|
||||||
static propTypes = {
|
interface IProps {
|
||||||
value: PropTypes.number.isRequired,
|
value: number;
|
||||||
// The maximum value that can be set with the power selector
|
// The maximum value that can be set with the power selector
|
||||||
maxValue: PropTypes.number.isRequired,
|
maxValue: number;
|
||||||
|
|
||||||
// Default user power level for the room
|
// Default user power level for the room
|
||||||
usersDefault: PropTypes.number.isRequired,
|
usersDefault: number;
|
||||||
|
|
||||||
// should the user be able to change the value? false by default.
|
// should the user be able to change the value? false by default.
|
||||||
disabled: PropTypes.bool,
|
disabled?: boolean;
|
||||||
onChange: PropTypes.func,
|
onChange?: (value: number, powerLevelKey: string) => void;
|
||||||
|
|
||||||
// Optional key to pass as the second argument to `onChange`
|
// Optional key to pass as the second argument to `onChange`
|
||||||
powerLevelKey: PropTypes.string,
|
powerLevelKey?: string;
|
||||||
|
|
||||||
// The name to annotate the selector with
|
// The name to annotate the selector with
|
||||||
label: PropTypes.string,
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
interface IState {
|
||||||
|
levelRoleMap: {};
|
||||||
|
// List of power levels to show in the drop-down
|
||||||
|
options: number[];
|
||||||
|
|
||||||
|
customValue: number;
|
||||||
|
selectValue: number | string;
|
||||||
|
custom?: boolean;
|
||||||
|
customLevel?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.PowerSelector")
|
||||||
|
export default class PowerSelector extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
maxValue: Infinity,
|
maxValue: Infinity,
|
||||||
usersDefault: 0,
|
usersDefault: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -62,26 +74,26 @@ export default class PowerSelector extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||||
UNSAFE_componentWillMount() {
|
public UNSAFE_componentWillMount(): void {
|
||||||
this._initStateFromProps(this.props);
|
this.initStateFromProps(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||||
this._initStateFromProps(newProps);
|
this.initStateFromProps(newProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initStateFromProps(newProps) {
|
private initStateFromProps(newProps: IProps): void {
|
||||||
// This needs to be done now because levelRoleMap has translated strings
|
// This needs to be done now because levelRoleMap has translated strings
|
||||||
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
|
||||||
const options = Object.keys(levelRoleMap).filter(level => {
|
const options = Object.keys(levelRoleMap).filter(level => {
|
||||||
return (
|
return (
|
||||||
level === undefined ||
|
level === undefined ||
|
||||||
level <= newProps.maxValue ||
|
parseInt(level) <= newProps.maxValue ||
|
||||||
level == newProps.value
|
parseInt(level) == newProps.value
|
||||||
);
|
);
|
||||||
});
|
}).map(level => parseInt(level));
|
||||||
|
|
||||||
const isCustom = levelRoleMap[newProps.value] === undefined;
|
const isCustom = levelRoleMap[newProps.value] === undefined;
|
||||||
|
|
||||||
|
@ -90,32 +102,33 @@ export default class PowerSelector extends React.Component {
|
||||||
options,
|
options,
|
||||||
custom: isCustom,
|
custom: isCustom,
|
||||||
customLevel: newProps.value,
|
customLevel: newProps.value,
|
||||||
selectValue: isCustom ? "SELECT_VALUE_CUSTOM" : newProps.value,
|
selectValue: isCustom ? CUSTOM_VALUE : newProps.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectChange = event => {
|
private onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||||
const isCustom = event.target.value === "SELECT_VALUE_CUSTOM";
|
const isCustom = event.target.value === CUSTOM_VALUE;
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
this.setState({ custom: true });
|
this.setState({ custom: true });
|
||||||
} else {
|
} else {
|
||||||
this.props.onChange(event.target.value, this.props.powerLevelKey);
|
const powerLevel = parseInt(event.target.value);
|
||||||
this.setState({ selectValue: event.target.value });
|
this.props.onChange(powerLevel, this.props.powerLevelKey);
|
||||||
|
this.setState({ selectValue: powerLevel });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomChange = event => {
|
private onCustomChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ customValue: event.target.value });
|
this.setState({ customValue: parseInt(event.target.value) });
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomBlur = event => {
|
private onCustomBlur = (event: React.FocusEvent): void => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey);
|
this.props.onChange(this.state.customValue, this.props.powerLevelKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCustomKeyDown = event => {
|
private onCustomKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||||
if (event.key === Key.ENTER) {
|
if (event.key === Key.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -125,11 +138,11 @@ export default class PowerSelector extends React.Component {
|
||||||
// raising a dialog which causes a blur which causes a dialog which causes a blur and
|
// raising a dialog which causes a blur which causes a dialog which causes a blur and
|
||||||
// so on. By not causing the onChange to be called here, we avoid the loop because we
|
// so on. By not causing the onChange to be called here, we avoid the loop because we
|
||||||
// handle the onBlur safely.
|
// handle the onBlur safely.
|
||||||
event.target.blur();
|
(event.target as HTMLInputElement).blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
let picker;
|
let picker;
|
||||||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
|
@ -147,14 +160,14 @@ export default class PowerSelector extends React.Component {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Each level must have a definition in this.state.levelRoleMap
|
// Each level must have a definition in this.state.levelRoleMap
|
||||||
let options = this.state.options.map((level) => {
|
const options = this.state.options.map((level) => {
|
||||||
return {
|
return {
|
||||||
value: level,
|
value: String(level),
|
||||||
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
text: Roles.textualPowerLevel(level, this.props.usersDefault),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
|
options.push({ value: CUSTOM_VALUE, text: _t("Custom level") });
|
||||||
options = options.map((op) => {
|
const optionsElements = options.map((op) => {
|
||||||
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
return <option value={op.value} key={op.value}>{ op.text }</option>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -166,7 +179,7 @@ export default class PowerSelector extends React.Component {
|
||||||
value={String(this.state.selectValue)}
|
value={String(this.state.selectValue)}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
{ options }
|
{ optionsElements }
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -17,25 +17,34 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
reason?: string;
|
||||||
|
contentHtml: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.Spoiler")
|
@replaceableComponent("views.elements.Spoiler")
|
||||||
export default class Spoiler extends React.Component {
|
export default class Spoiler extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisible(e) {
|
private toggleVisible = (e: React.MouseEvent): void => {
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
// we are un-blurring, we don't want this click to propagate to potential child pills
|
// we are un-blurring, we don't want this click to propagate to potential child pills
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
this.setState({ visible: !this.state.visible });
|
this.setState({ visible: !this.state.visible });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const reason = this.props.reason ? (
|
const reason = this.props.reason ? (
|
||||||
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
|
<span className="mx_EventTile_spoiler_reason">{ "(" + this.props.reason + ")" }</span>
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -43,7 +52,7 @@ export default class Spoiler extends React.Component {
|
||||||
// as such, we pass the this.props.contentHtml instead and then set the raw
|
// as such, we pass the this.props.contentHtml instead and then set the raw
|
||||||
// HTML content. This is secure as the contents have already been parsed previously
|
// HTML content. This is secure as the contents have already been parsed previously
|
||||||
return (
|
return (
|
||||||
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible.bind(this)}>
|
<span className={"mx_EventTile_spoiler" + (this.state.visible ? " visible" : "")} onClick={this.toggleVisible}>
|
||||||
{ reason }
|
{ reason }
|
||||||
|
|
||||||
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
<span className="mx_EventTile_spoiler_content" dangerouslySetInnerHTML={{ __html: this.props.contentHtml }} />
|
|
@ -15,40 +15,40 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { highlightBlock } from 'highlight.js';
|
import { highlightBlock } from 'highlight.js';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.SyntaxHighlight")
|
@replaceableComponent("views.elements.SyntaxHighlight")
|
||||||
export default class SyntaxHighlight extends React.Component {
|
export default class SyntaxHighlight extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private el: HTMLPreElement = null;
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._ref = this._ref.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// componentDidUpdate used here for reusability
|
// componentDidUpdate used here for reusability
|
||||||
componentDidUpdate() {
|
public componentDidUpdate(): void {
|
||||||
if (this._el) highlightBlock(this._el);
|
if (this.el) highlightBlock(this.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
// call componentDidUpdate because _ref is fired on initial render
|
// call componentDidUpdate because _ref is fired on initial render
|
||||||
// which does not fire componentDidUpdate
|
// which does not fire componentDidUpdate
|
||||||
_ref(el) {
|
private ref = (el: HTMLPreElement): void => {
|
||||||
this._el = el;
|
this.el = el;
|
||||||
this.componentDidUpdate();
|
this.componentDidUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const { className, children } = this.props;
|
const { className, children } = this.props;
|
||||||
|
|
||||||
return <pre className={`${className} mx_SyntaxHighlight`} ref={this._ref}>
|
return <pre className={`${className} mx_SyntaxHighlight`} ref={this.ref}>
|
||||||
<code>{ children }</code>
|
<code>{ children }</code>
|
||||||
</pre>;
|
</pre>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,42 +15,44 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
class?: string;
|
||||||
|
tooltipClass?: string;
|
||||||
|
tooltip: React.ReactNode;
|
||||||
|
tooltipProps?: {};
|
||||||
|
onClick?: (ev?: React.MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hover: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.TextWithTooltip")
|
@replaceableComponent("views.elements.TextWithTooltip")
|
||||||
export default class TextWithTooltip extends React.Component {
|
export default class TextWithTooltip extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
class: PropTypes.string,
|
super(props);
|
||||||
tooltipClass: PropTypes.string,
|
|
||||||
tooltip: PropTypes.node.isRequired,
|
|
||||||
tooltipProps: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hover: false,
|
hover: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseOver = () => {
|
private onMouseOver = (): void => {
|
||||||
this.setState({ hover: true });
|
this.setState({ hover: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
onMouseLeave = () => {
|
private onMouseLeave = (): void => {
|
||||||
this.setState({ hover: false });
|
this.setState({ hover: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render(): JSX.Element {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
|
||||||
|
|
||||||
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
|
const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className={className}>
|
<span {...props} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick} className={className}>
|
||||||
{ children }
|
{ children }
|
||||||
{ this.state.hover && <Tooltip
|
{ this.state.hover && <Tooltip
|
||||||
{...tooltipProps}
|
{...tooltipProps}
|
|
@ -15,20 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
import QRCode from "../QRCode";
|
import QRCode from "../QRCode";
|
||||||
|
import { QRCodeData } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
qrCodeData: QRCodeData;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||||
export default class VerificationQRCode extends React.PureComponent {
|
export default class VerificationQRCode extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
public render(): JSX.Element {
|
||||||
qrCodeData: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<QRCode
|
<QRCode
|
||||||
data={[{ data: this.props.qrCodeData.buffer, mode: 'byte' }]}
|
data={[{ data: this.props.qrCodeData.getBuffer(), mode: 'byte' }]}
|
||||||
className="mx_VerificationQRCode"
|
className="mx_VerificationQRCode"
|
||||||
width={196} />
|
width={196} />
|
||||||
);
|
);
|
|
@ -1052,8 +1052,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
const onPowerChange = useCallback(async (powerLevel: number) => {
|
||||||
const powerLevel = parseInt(powerLevelStr, 10);
|
|
||||||
setSelectedPowerLevel(powerLevel);
|
setSelectedPowerLevel(powerLevel);
|
||||||
|
|
||||||
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||||
|
|
|
@ -97,7 +97,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
<AppTile
|
<AppTile
|
||||||
app={app}
|
app={app}
|
||||||
fullWidth
|
fullWidth
|
||||||
show
|
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
room={room}
|
room={room}
|
||||||
userId={cli.getUserId()}
|
userId={cli.getUserId()}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||||
|
import { IApp } from "../../../stores/WidgetStore";
|
||||||
|
|
||||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||||
// We sit in a context menu, so this should be given to the context menu.
|
// We sit in a context menu, so this should be given to the context menu.
|
||||||
|
@ -256,12 +257,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
stickerpickerWidget.content.name = stickerpickerWidget.content.name || _t("Stickerpack");
|
||||||
|
|
||||||
// FIXME: could this use the same code as other apps?
|
// FIXME: could this use the same code as other apps?
|
||||||
const stickerApp = {
|
const stickerApp: IApp = {
|
||||||
id: stickerpickerWidget.id,
|
id: stickerpickerWidget.id,
|
||||||
url: stickerpickerWidget.content.url,
|
url: stickerpickerWidget.content.url,
|
||||||
name: stickerpickerWidget.content.name,
|
name: stickerpickerWidget.content.name,
|
||||||
type: stickerpickerWidget.content.type,
|
type: stickerpickerWidget.content.type,
|
||||||
data: stickerpickerWidget.content.data,
|
data: stickerpickerWidget.content.data,
|
||||||
|
roomId: stickerpickerWidget.content.roomId,
|
||||||
|
eventId: stickerpickerWidget.content.eventId,
|
||||||
|
avatar_url: stickerpickerWidget.content.avatar_url,
|
||||||
|
creatorUserId: stickerpickerWidget.content.creatorUserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
stickersContent = (
|
stickersContent = (
|
||||||
|
@ -287,9 +292,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
onEditClick={this.launchManageIntegrations}
|
onEditClick={this.launchManageIntegrations}
|
||||||
onDeleteClick={this.removeStickerpickerWidgets}
|
onDeleteClick={this.removeStickerpickerWidgets}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showCancel={false}
|
|
||||||
showPopout={false}
|
showPopout={false}
|
||||||
onMinimiseClick={this.onHideStickersClick}
|
|
||||||
handleMinimisePointerEvents={true}
|
handleMinimisePointerEvents={true}
|
||||||
userWidget={true}
|
userWidget={true}
|
||||||
/>
|
/>
|
||||||
|
@ -345,16 +348,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger hiding of the sticker picker overlay
|
|
||||||
* @param {Event} ev Event that triggered the function call
|
|
||||||
*/
|
|
||||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
|
||||||
if (this.props.showStickers) {
|
|
||||||
this.props.setShowStickers(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the window is resized
|
* Called when the window is resized
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -137,7 +137,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
|
private onPowerLevelsChanged = (value: number, powerLevelKey: string) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||||
|
@ -148,8 +148,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
|
|
||||||
const eventsLevelPrefix = "event_levels_";
|
const eventsLevelPrefix = "event_levels_";
|
||||||
|
|
||||||
const value = parseInt(inputValue);
|
|
||||||
|
|
||||||
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
|
||||||
// deep copy "events" object, Object.assign itself won't deep copy
|
// deep copy "events" object, Object.assign itself won't deep copy
|
||||||
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
plContent["events"] = Object.assign({}, plContent["events"] || {});
|
||||||
|
@ -181,7 +179,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
|
private onUserPowerLevelChanged = (value: number, powerLevelKey: string) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, '');
|
||||||
|
|
|
@ -67,7 +67,7 @@ interface IAppTileProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
creatorUserId: string;
|
creatorUserId: string;
|
||||||
waitForIframeLoad: boolean;
|
waitForIframeLoad: boolean;
|
||||||
whitelistCapabilities: string[];
|
whitelistCapabilities?: string[];
|
||||||
userWidget: boolean;
|
userWidget: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue