Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Weblate 2018-05-11 17:13:03 +00:00
commit 125082a76d
6 changed files with 275 additions and 82 deletions

View File

@ -34,7 +34,7 @@ limitations under the License.
width: 100%; width: 100%;
} }
.mx_MessageComposer_row div:last-child{ .mx_MessageComposer_row > div:last-child{
padding-right: 0; padding-right: 0;
} }

View File

@ -94,6 +94,16 @@ export default class WidgetMessaging {
}); });
} }
sendVisibility(visible) {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: "visibility",
visible,
})
.catch((error) => {
console.error("Failed to send visibility: ", error);
});
}
start() { start() {
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);

View File

@ -26,9 +26,21 @@ import PropTypes from 'prop-types';
// 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.
module.exports = { const ContextualMenuContainerId = "mx_ContextualMenu_Container";
ContextualMenuContainerId: "mx_ContextualMenu_Container",
function getOrCreateContainer() {
let container = document.getElementById(ContextualMenuContainerId);
if (!container) {
container = document.createElement("div");
container.id = ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
}
export default class ContextualMenu extends React.Component {
propTypes: { propTypes: {
top: PropTypes.number, top: PropTypes.number,
bottom: PropTypes.number, bottom: PropTypes.number,
@ -45,39 +57,18 @@ module.exports = {
menuPaddingRight: PropTypes.number, menuPaddingRight: PropTypes.number,
menuPaddingBottom: PropTypes.number, menuPaddingBottom: PropTypes.number,
menuPaddingLeft: PropTypes.number, menuPaddingLeft: PropTypes.number,
},
getOrCreateContainer: function() { // If true, insert an invisible screen-sized element behind the
let container = document.getElementById(this.ContextualMenuContainerId); // menu that when clicked will close it.
hasBackground: PropTypes.bool,
if (!container) { }
container = document.createElement("div");
container.id = this.ContextualMenuContainerId;
document.body.appendChild(container);
}
return container;
},
createMenu: function(Element, props) {
const self = this;
const closeMenu = function(...args) {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) {
props.onFinished.apply(null, args);
}
};
// Close the menu on window resize
const windowResize = function() {
closeMenu();
};
render() {
const position = {}; const position = {};
let chevronFace = null; let chevronFace = null;
const props = this.props;
if (props.top) { if (props.top) {
position.top = props.top; position.top = props.top;
} else { } else {
@ -158,21 +149,40 @@ module.exports = {
menuStyle["paddingRight"] = props.menuPaddingRight; menuStyle["paddingRight"] = props.menuPaddingRight;
} }
const ElementClass = props.elementClass;
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
const menu = ( return <div className={className} style={position}>
<div className={className} style={position}> <div className={menuClasses} style={menuStyle}>
<div className={menuClasses} style={menuStyle}> { chevron }
{ chevron } <ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
<Element {...props} onFinished={closeMenu} onResize={windowResize} />
</div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
<style>{ chevronCSS }</style>
</div> </div>
); { props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu}></div> }
<style>{ chevronCSS }</style>
</div>;
}
}
ReactDOM.render(menu, this.getOrCreateContainer()); export function createMenu(ElementClass, props) {
const closeMenu = function(...args) {
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
return {close: closeMenu}; if (props && props.onFinished) {
}, props.onFinished.apply(null, args);
}; }
};
// We only reference closeMenu once per call to createMenu
const menu = <ContextualMenu
{...props}
hasBackground={true}
elementClass={ElementClass}
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
/>;
ReactDOM.render(menu, getOrCreateContainer());
return {close: closeMenu};
}

View File

@ -169,6 +169,13 @@ export default class AppTile extends React.Component {
this.dispatcherRef = dis.register(this._onWidgetAction); this.dispatcherRef = dis.register(this._onWidgetAction);
} }
componentDidUpdate() {
// Allow parents to access widget messaging
if (this.props.collectWidgetMessaging) {
this.props.collectWidgetMessaging(this.widgetMessaging);
}
}
componentWillUnmount() { componentWillUnmount() {
// Widget action listeners // Widget action listeners
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
@ -357,6 +364,9 @@ export default class AppTile extends React.Component {
if (!this.widgetMessaging) { if (!this.widgetMessaging) {
this._onInitialLoad(); this._onInitialLoad();
} }
if (this._exposeWidgetMessaging) {
this._exposeWidgetMessaging(this.widgetMessaging);
}
} }
/** /**
@ -394,6 +404,7 @@ export default class AppTile extends React.Component {
}).catch((err) => { }).catch((err) => {
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err); console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
}); });
this.setState({loading: false}); this.setState({loading: false});
} }

View File

@ -0,0 +1,114 @@
/*
Copyright 2018 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const React = require('react');
const ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
const ContainerId = "mx_PersistedElement";
function getOrCreateContainer() {
let container = document.getElementById(ContainerId);
if (!container) {
container = document.createElement("div");
container.id = ContainerId;
document.body.appendChild(container);
}
return container;
}
// Greater than that of the ContextualMenu
const PE_Z_INDEX = 3000;
/*
* Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body.
*
* This prevents the children from being unmounted when the parent of PersistedElement
* unmounts, allowing them to persist.
*
* When PE is unmounted, it hides the children using CSS. When mounted or updated, the
* children are made visible and are positioned into a div that is given the same
* bounding rect as the parent of PE.
*/
export default class PersistedElement extends React.Component {
constructor() {
super();
this.collectChildContainer = this.collectChildContainer.bind(this);
this.collectChild = this.collectChild.bind(this);
}
collectChildContainer(ref) {
this.childContainer = ref;
}
collectChild(ref) {
this.child = ref;
this.updateChild();
}
componentDidMount() {
this.updateChild();
}
componentDidUpdate() {
this.updateChild();
}
componentWillUnmount() {
this.updateChildVisibility(this.child, false);
}
updateChild() {
this.updateChildPosition(this.child, this.childContainer);
this.updateChildVisibility(this.child, true);
}
updateChildVisibility(child, visible) {
if (!child) return;
child.style.display = visible ? 'block' : 'none';
}
updateChildPosition(child, parent) {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
position: 'absolute',
top: parentRect.top + 'px',
left: parentRect.left + 'px',
width: parentRect.width + 'px',
height: parentRect.height + 'px',
zIndex: PE_Z_INDEX,
});
}
render() {
const content = <div ref={this.collectChild}>
{this.props.children}
</div>;
ReactDOM.render(content, getOrCreateContainer());
return <div ref={this.collectChildContainer}></div>;
}
}

View File

@ -17,7 +17,6 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Widgets from '../../../utils/widgets'; import Widgets from '../../../utils/widgets';
import AppTile from '../elements/AppTile'; import AppTile from '../elements/AppTile';
import ContextualMenu from '../../structures/ContextualMenu';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
@ -36,21 +35,28 @@ export default class Stickerpicker extends React.Component {
this._launchManageIntegrations = this._launchManageIntegrations.bind(this); this._launchManageIntegrations = this._launchManageIntegrations.bind(this);
this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this); this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this);
this._onWidgetAction = this._onWidgetAction.bind(this); this._onWidgetAction = this._onWidgetAction.bind(this);
this._onResize = this._onResize.bind(this);
this._onFinished = this._onFinished.bind(this); this._onFinished = this._onFinished.bind(this);
this._collectWidgetMessaging = this._collectWidgetMessaging.bind(this);
this.popoverWidth = 300; this.popoverWidth = 300;
this.popoverHeight = 300; this.popoverHeight = 300;
this.state = { this.state = {
showStickers: false, showStickers: false,
imError: null, imError: null,
stickerpickerX: null,
stickerpickerY: null,
stickerpickerWidget: null,
widgetId: null,
}; };
} }
_removeStickerpickerWidgets() { _removeStickerpickerWidgets() {
console.warn('Removing Stickerpicker widgets'); console.warn('Removing Stickerpicker widgets');
if (this.widgetId) { if (this.state.widgetId) {
this.scalarClient.disableWidgetAssets(widgetType, this.widgetId).then(() => { this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
console.warn('Assets disabled'); console.warn('Assets disabled');
}).catch((err) => { }).catch((err) => {
console.error('Failed to disable assets'); console.error('Failed to disable assets');
@ -59,8 +65,7 @@ export default class Stickerpicker extends React.Component {
console.warn('No widget ID specified, not disabling assets'); console.warn('No widget ID specified, not disabling assets');
} }
// Wrap this in a timeout in order to avoid the DOM node from being pulled from under its feet this.setState({showStickers: false});
setTimeout(() => this.stickersMenu.close());
Widgets.removeStickerpickerWidgets().then(() => { Widgets.removeStickerpickerWidgets().then(() => {
this.forceUpdate(); this.forceUpdate();
}).catch((e) => { }).catch((e) => {
@ -69,6 +74,9 @@ export default class Stickerpicker extends React.Component {
} }
componentDidMount() { componentDidMount() {
// Close the sticker picker when the window resizes
window.addEventListener('resize', this._onResize);
this.scalarClient = null; this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient(); this.scalarClient = new ScalarAuthClient();
@ -82,14 +90,24 @@ export default class Stickerpicker extends React.Component {
if (!this.state.imError) { if (!this.state.imError) {
this.dispatcherRef = dis.register(this._onWidgetAction); this.dispatcherRef = dis.register(this._onWidgetAction);
} }
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
this.setState({
stickerpickerWidget,
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
});
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('resize', this._onResize);
if (this.dispatcherRef) { if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
} }
componentDidUpdate(prevProps, prevState) {
this._sendVisibilityToWidget(this.state.showStickers);
}
_imError(errorMsg, e) { _imError(errorMsg, e) {
console.error(errorMsg, e); console.error(errorMsg, e);
this.setState({ this.setState({
@ -102,9 +120,7 @@ export default class Stickerpicker extends React.Component {
if (payload.action === "user_widget_updated") { if (payload.action === "user_widget_updated") {
this.forceUpdate(); this.forceUpdate();
} else if (payload.action === "stickerpicker_close") { } else if (payload.action === "stickerpicker_close") {
// Wrap this in a timeout in order to avoid the DOM node from being this.setState({showStickers: false});
// pulled from under its feet
setTimeout(() => this.stickersMenu.close());
} }
} }
@ -127,6 +143,21 @@ export default class Stickerpicker extends React.Component {
); );
} }
_collectWidgetMessaging(widgetMessaging) {
this._appWidgetMessaging = widgetMessaging;
// Do this now instead of in componentDidMount because we might not have had the
// reference to widgetMessaging when mounting
this._sendVisibilityToWidget(true);
}
_sendVisibilityToWidget(visible) {
if (this._appWidgetMessaging && visible !== this._prevSentVisibility) {
this._appWidgetMessaging.sendVisibility(visible);
this._prevSentVisibility = visible;
}
}
_getStickerpickerContent() { _getStickerpickerContent() {
// Handle Integration Manager errors // Handle Integration Manager errors
if (this.state._imError) { if (this.state._imError) {
@ -137,14 +168,18 @@ export default class Stickerpicker extends React.Component {
// TODO - Add support for Stickerpickers from multiple app stores. // TODO - Add support for Stickerpickers from multiple app stores.
// Render content from multiple stickerpack sources, each within their // Render content from multiple stickerpack sources, each within their
// own iframe, within the stickerpicker UI element. // own iframe, within the stickerpicker UI element.
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0]; const stickerpickerWidget = this.state.stickerpickerWidget;
let stickersContent; let stickersContent;
// Use a separate ReactDOM tree to render the AppTile separately so that it persists and does
// not unmount when we (a) close the sticker picker (b) switch rooms. It's properties are still
// updated.
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Load stickerpack content // Load stickerpack content
if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) { if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
// Set default name // Set default name
stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack"); stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");
this.widgetId = stickerpickerWidget.id;
stickersContent = ( stickersContent = (
<div className='mx_Stickers_content_container'> <div className='mx_Stickers_content_container'>
@ -157,7 +192,9 @@ export default class Stickerpicker extends React.Component {
width: this.popoverWidth, width: this.popoverWidth,
}} }}
> >
<PersistedElement>
<AppTile <AppTile
collectWidgetMessaging={this._collectWidgetMessaging}
id={stickerpickerWidget.id} id={stickerpickerWidget.id}
url={stickerpickerWidget.content.url} url={stickerpickerWidget.content.url}
name={stickerpickerWidget.content.name} name={stickerpickerWidget.content.name}
@ -177,9 +214,10 @@ export default class Stickerpicker extends React.Component {
showPopout={false} showPopout={false}
onMinimiseClick={this._onHideStickersClick} onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
whitelistCapabilities={['m.sticker']} whitelistCapabilities={['m.sticker', 'visibility']}
userWidget={true} userWidget={true}
/> />
</PersistedElement>
</div> </div>
</div> </div>
); );
@ -187,12 +225,7 @@ export default class Stickerpicker extends React.Component {
// Default content to show if stickerpicker widget not added // Default content to show if stickerpicker widget not added
console.warn("No available sticker picker widgets"); console.warn("No available sticker picker widgets");
stickersContent = this._defaultStickerpickerContent(); stickersContent = this._defaultStickerpickerContent();
this.widgetId = null;
this.forceUpdate();
} }
this.setState({
showStickers: false,
});
return stickersContent; return stickersContent;
} }
@ -202,29 +235,17 @@ export default class Stickerpicker extends React.Component {
* @param {Event} e Event that triggered the function * @param {Event} e Event that triggered the function
*/ */
_onShowStickersClick(e) { _onShowStickersClick(e) {
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
const buttonRect = e.target.getBoundingClientRect(); const buttonRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page // The window X and Y offsets are to adjust position when zoomed in to page
const x = buttonRect.right + window.pageXOffset - 42; const x = buttonRect.right + window.pageXOffset - 42;
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
// const self = this;
this.stickersMenu = ContextualMenu.createMenu(GenericElementContextMenu, { this.setState({
chevronOffset: 10, showStickers: true,
chevronFace: 'bottom', stickerPickerX: x,
left: x, stickerPickerY: y,
top: y,
menuWidth: this.popoverWidth,
menuHeight: this.popoverHeight,
element: this._getStickerpickerContent(),
onFinished: this._onFinished,
menuPaddingTop: 0,
menuPaddingLeft: 0,
menuPaddingRight: 0,
}); });
this.setState({showStickers: true});
} }
/** /**
@ -232,7 +253,14 @@ export default class Stickerpicker extends React.Component {
* @param {Event} ev Event that triggered the function call * @param {Event} ev Event that triggered the function call
*/ */
_onHideStickersClick(ev) { _onHideStickersClick(ev) {
setTimeout(() => this.stickersMenu.close()); this.setState({showStickers: false});
}
/**
* Called when the window is resized
*/
_onResize() {
this.setState({showStickers: false});
} }
/** /**
@ -251,20 +279,37 @@ export default class Stickerpicker extends React.Component {
this.scalarClient.getScalarInterfaceUrlForRoom( this.scalarClient.getScalarInterfaceUrlForRoom(
this.props.room, this.props.room,
'type_' + widgetType, 'type_' + widgetType,
this.widgetId, this.state.widgetId,
) : ) :
null; null;
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src, src: src,
}, "mx_IntegrationsManager"); }, "mx_IntegrationsManager");
// Wrap this in a timeout in order to avoid the DOM node from being pulled from under its feet this.setState({showStickers: false});
setTimeout(() => this.stickersMenu.close());
} }
render() { render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
let stickersButton; let stickersButton;
const stickerPicker = <ContextualMenu
elementClass={GenericElementContextMenu}
chevronOffset={10}
chevronFace={'bottom'}
left={this.state.stickerPickerX}
top={this.state.stickerPickerY}
menuWidth={this.popoverWidth}
menuHeight={this.popoverHeight}
element={this._getStickerpickerContent()}
onFinished={this._onFinished}
menuPaddingTop={0}
menuPaddingLeft={0}
menuPaddingRight={0}
/>;
if (this.state.showStickers) { if (this.state.showStickers) {
// Show hide-stickers button // Show hide-stickers button
stickersButton = stickersButton =
@ -289,6 +334,9 @@ export default class Stickerpicker extends React.Component {
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" /> <TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
</div>; </div>;
} }
return stickersButton; return <div>
{stickersButton}
{this.state.showStickers && stickerPicker}
</div>;
} }
} }