Merge pull request #1672 from matrix-org/rxl881/snapshot
Bi-directional widget postMessaging API (stickerpacks) [WIP]
This commit is contained in:
commit
06f1c504e7
27 changed files with 1650 additions and 556 deletions
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
@ -36,32 +36,23 @@ import WidgetUtils from '../../../WidgetUtils';
|
|||
import dis from '../../../dispatcher';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'AppTile',
|
||||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
propTypes: {
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
// 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,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
creatorUserId: PropTypes.string,
|
||||
waitForIframeLoad: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
};
|
||||
},
|
||||
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||
|
@ -73,8 +64,8 @@ export default React.createClass({
|
|||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
|
@ -83,8 +74,20 @@ export default React.createClass({
|
|||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
|
||||
this.props.whitelistCapabilities : [],
|
||||
requestedCapabilities: [],
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the widget support a given capability
|
||||
* @param {[type]} capability Capability to check for
|
||||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return this.state.allowedCapabilities.some((c) => {return c === capability;});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add widget instance specific parameters to pass in wUrl
|
||||
|
@ -112,11 +115,7 @@ export default React.createClass({
|
|||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return this._getNewState(this.props);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
|
@ -140,7 +139,7 @@ export default React.createClass({
|
|||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
|
@ -152,14 +151,36 @@ export default React.createClass({
|
|||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
WidgetMessaging.startListening();
|
||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
this.setScalarToken();
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Legacy Jitsi widget messaging -- TODO replace this with standard widget
|
||||
// postMessaging API
|
||||
window.addEventListener('message', this._onMessage, false);
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Widget postMessage listeners
|
||||
try {
|
||||
if (this.widgetMessaging) {
|
||||
this.widgetMessaging.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||
}
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a scalar token to the widget URL, if required
|
||||
|
@ -211,13 +232,7 @@ export default React.createClass({
|
|||
initialising: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
WidgetMessaging.stopListening();
|
||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.url !== this.props.url) {
|
||||
|
@ -232,8 +247,10 @@ export default React.createClass({
|
|||
widgetPageTitle: nextProps.widgetPageTitle,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Legacy Jitsi widget messaging
|
||||
// TODO -- This should be replaced with the new widget postMessaging API
|
||||
_onMessage(event) {
|
||||
if (this.props.type !== 'jitsi') {
|
||||
return;
|
||||
|
@ -251,63 +268,140 @@ export default React.createClass({
|
|||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_canUserModify() {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
},
|
||||
}
|
||||
|
||||
_onEditClick(e) {
|
||||
console.log("Edit widget ID ", this.props.id);
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
},
|
||||
if (this.props.onEditClick) {
|
||||
this.props.onEditClick();
|
||||
} else {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
}
|
||||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
this.widgetMessaging.getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
.then((screenshot) => {
|
||||
dis.dispatch({
|
||||
action: 'picture_snapshot',
|
||||
file: screenshot,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
/* If user has permission to modify widgets, delete the widget,
|
||||
* otherwise revoke access for the widget to load in the user's browser
|
||||
*/
|
||||
_onDeleteClick() {
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
if (this.props.onDeleteClick) {
|
||||
this.props.onDeleteClick();
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
if (this._canUserModify()) {
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
this._revokeWidgetPermission();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
if (!this.widgetMessaging) {
|
||||
this._onInitialLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on initial load of the widget iframe
|
||||
*/
|
||||
_onInitialLoad() {
|
||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
let requestedWhitelistCapabilies = [];
|
||||
|
||||
if (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) {
|
||||
requestedWhitelistCapabilies = requestedCapabilities.filter(function(e) {
|
||||
return this.indexOf(e)>=0;
|
||||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
|
||||
requestedWhitelistCapabilies);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
this.setState({
|
||||
requestedCapabilities,
|
||||
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
|
||||
});
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err);
|
||||
});
|
||||
this.setState({loading: false});
|
||||
},
|
||||
}
|
||||
|
||||
_onWidgetAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
if (this._hasCapability('m.sticker')) {
|
||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||
} else {
|
||||
console.warn('Ignoring sticker message. Invalid capability');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remote content title on AppTile
|
||||
|
@ -321,7 +415,7 @@ export default React.createClass({
|
|||
}, (err) =>{
|
||||
console.error("Failed to get page title", err);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
// Widget labels to render, depending upon user permissions
|
||||
// These strings are translated at the point that they are inserted in to the DOM, in the render method
|
||||
|
@ -330,20 +424,20 @@ export default React.createClass({
|
|||
return _td('Delete widget');
|
||||
}
|
||||
return _td('Revoke widget access');
|
||||
},
|
||||
}
|
||||
|
||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||
_grantWidgetPermission() {
|
||||
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||
this.setState({hasPermissionToLoad: true});
|
||||
},
|
||||
}
|
||||
|
||||
_revokeWidgetPermission() {
|
||||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.removeItem(this.state.widgetPermissionId);
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
},
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
let appTileName = "No name";
|
||||
|
@ -351,7 +445,7 @@ export default React.createClass({
|
|||
appTileName = this.props.name.trim();
|
||||
}
|
||||
return appTileName;
|
||||
},
|
||||
}
|
||||
|
||||
onClickMenuBar(ev) {
|
||||
ev.preventDefault();
|
||||
|
@ -366,16 +460,42 @@ export default React.createClass({
|
|||
action: 'appsDrawer',
|
||||
show: !this.props.show,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_getSafeUrl() {
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl, true);
|
||||
if (ENABLE_REACT_PERF) {
|
||||
parsedWidgetUrl.search = null;
|
||||
parsedWidgetUrl.query.react_perf = true;
|
||||
}
|
||||
let safeWidgetUrl = '';
|
||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||
}
|
||||
return safeWidgetUrl;
|
||||
},
|
||||
}
|
||||
|
||||
_getTileTitle() {
|
||||
const name = this.formatAppTileName();
|
||||
const titleSpacer = <span> - </span>;
|
||||
let title = '';
|
||||
if (this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName()) {
|
||||
title = this.state.widgetPageTitle;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<b>{ name }</b>
|
||||
<span>{ title ? titleSpacer : '' }{ title }</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
_onMinimiseClick(e) {
|
||||
if (this.props.onMinimiseClick) {
|
||||
this.props.onMinimiseClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
@ -399,7 +519,7 @@ export default React.createClass({
|
|||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
<div>
|
||||
<MessageSpinner msg='Loading...' />
|
||||
</div>
|
||||
);
|
||||
|
@ -414,7 +534,7 @@ export default React.createClass({
|
|||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
|
@ -456,29 +576,42 @@ export default React.createClass({
|
|||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
// Picture snapshot - only show button when apps are maximised.
|
||||
const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle">
|
||||
<TintableSvgButton
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
{ this.props.showMinimise && <TintableSvgButton
|
||||
src={windowStateIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Minimize apps')}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
<b>{ this.formatAppTileName() }</b>
|
||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||
<span> - { this.state.widgetPageTitle }</span>
|
||||
) }
|
||||
onClick={this._onMinimiseClick}
|
||||
/> }
|
||||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Snapshot widget */ }
|
||||
{ showPictureSnapshotButton && <TintableSvgButton
|
||||
src={showPictureSnapshotIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Picture')}
|
||||
onClick={this._onSnapshotClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Edit widget */ }
|
||||
{ showEditButton && <TintableSvgButton
|
||||
src="img/edit_green.svg"
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
className={"mx_AppTileMenuBarWidget " +
|
||||
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||
title={_t('Edit')}
|
||||
onClick={this._onEditClick}
|
||||
width="10"
|
||||
|
@ -486,18 +619,71 @@ export default React.createClass({
|
|||
/> }
|
||||
|
||||
{ /* Delete widget */ }
|
||||
<TintableSvgButton
|
||||
{ this.props.showDelete && <TintableSvgButton
|
||||
src={deleteIcon}
|
||||
className={deleteClasses}
|
||||
title={_t(deleteWidgetLabel)}
|
||||
onClick={this._onDeleteClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/>
|
||||
/> }
|
||||
</span>
|
||||
</div>
|
||||
</div> }
|
||||
{ appTileBody }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
AppTile.displayName ='AppTile';
|
||||
|
||||
AppTile.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
// 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,
|
||||
// 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,
|
||||
// Should the AppTile render itself
|
||||
show: 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 hide the tile minimise icon
|
||||
showMinimise: PropTypes.bool,
|
||||
// Optionally handle minimise button pointer events (default false)
|
||||
handleMinimisePointerEvents: PropTypes.bool,
|
||||
// Optionally hide the delete icon
|
||||
showDelete: PropTypes.bool,
|
||||
// Widget apabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
whitelistCapabilities: PropTypes.array,
|
||||
// Optional function to be called on widget capability request
|
||||
// Called with an array of the requested capabilities
|
||||
onCapabilityRequest: PropTypes.func,
|
||||
};
|
||||
|
||||
AppTile.defaultProps = {
|
||||
url: "",
|
||||
waitForIframeLoad: true,
|
||||
showMenubar: true,
|
||||
showTitle: true,
|
||||
showMinimise: true,
|
||||
showDelete: true,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
};
|
||||
|
|
|
@ -64,7 +64,7 @@ export default class ManageIntegsButton extends React.Component {
|
|||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
Modal.createDialog(IntegrationsManager, {
|
||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||
null,
|
||||
}, "mx_IntegrationsManager");
|
||||
}
|
||||
|
@ -103,5 +103,5 @@ export default class ManageIntegsButton extends React.Component {
|
|||
}
|
||||
|
||||
ManageIntegsButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -30,35 +30,45 @@ import Promise from 'bluebird';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
export default class extends React.Component {
|
||||
displayName: 'MImageBody'
|
||||
|
||||
propTypes: {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
},
|
||||
}
|
||||
|
||||
contextTypes: {
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.fixupHeight = this.fixupHeight.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
decryptedUrl: null,
|
||||
decryptedThumbnailUrl: null,
|
||||
decryptedBlob: null,
|
||||
error: null,
|
||||
imgError: false,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
},
|
||||
}
|
||||
|
||||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
@ -71,9 +81,9 @@ module.exports = React.createClass({
|
|||
imgError: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onClick: function onClick(ev) {
|
||||
onClick(ev) {
|
||||
if (ev.button == 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -93,49 +103,49 @@ module.exports = React.createClass({
|
|||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_isGif: function() {
|
||||
_isGif() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
content &&
|
||||
content.info &&
|
||||
content.info.mimetype === "image/gif"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
onImageEnter: function(e) {
|
||||
onImageEnter(e) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getContentUrl();
|
||||
},
|
||||
}
|
||||
|
||||
onImageLeave: function(e) {
|
||||
onImageLeave(e) {
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
const imgElement = e.target;
|
||||
imgElement.src = this._getThumbUrl();
|
||||
},
|
||||
}
|
||||
|
||||
onImageError: function() {
|
||||
onImageError() {
|
||||
this.setState({
|
||||
imgError: true,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_getContentUrl: function() {
|
||||
_getContentUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
return this.state.decryptedUrl;
|
||||
} else {
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_getThumbUrl: function() {
|
||||
_getThumbUrl() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined) {
|
||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||
|
@ -146,9 +156,9 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.fixupHeight();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
@ -182,23 +192,35 @@ module.exports = React.createClass({
|
|||
});
|
||||
}).done();
|
||||
}
|
||||
},
|
||||
this._afterComponentDidMount();
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||
// initialisation after componentDidMount
|
||||
_afterComponentDidMount() {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
this._afterComponentWillUnmount();
|
||||
}
|
||||
|
||||
onAction: function(payload) {
|
||||
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||
// cleanup after componentWillUnmount
|
||||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "timeline_resize") {
|
||||
this.fixupHeight();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fixupHeight: function() {
|
||||
fixupHeight() {
|
||||
if (!this.refs.image) {
|
||||
console.warn("Refusing to fix up height on MImageBody with no image element");
|
||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -214,10 +236,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
this.refs.image.style.height = thumbHeight + "px";
|
||||
// console.log("Image height now", thumbHeight);
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
|
||||
if (this.state.error !== null) {
|
||||
|
@ -265,19 +302,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
if (thumbUrl) {
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.props.onWidgetLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
return this._messageContent(contentUrl, thumbUrl, content);
|
||||
} else if (content.body) {
|
||||
return (
|
||||
<span className="mx_MImageBody">
|
||||
|
@ -291,5 +316,5 @@ module.exports = React.createClass({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
149
src/components/views/messages/MStickerBody.js
Normal file
149
src/components/views/messages/MStickerBody.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
import TintableSVG from '../elements/TintableSvg';
|
||||
|
||||
export default class MStickerBody extends MImageBody {
|
||||
displayName: 'MStickerBody'
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onImageLoad = this._onImageLoad.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.setState({showTooltip: true});
|
||||
}
|
||||
|
||||
_onMouseLeave() {
|
||||
this.setState({showTooltip: false});
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.setState({
|
||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
||||
});
|
||||
const hidePlaceholderTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
}, 500);
|
||||
this.setState({hidePlaceholderTimer});
|
||||
if (this.props.onWidgetLoad) {
|
||||
this.props.onWidgetLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentDidMount() {
|
||||
if (this.refs.image.complete) {
|
||||
// Image already loaded
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
} else {
|
||||
// Image not already loaded
|
||||
this.setState({
|
||||
placeholderVisible: true,
|
||||
placeholderClasses: '',
|
||||
imageClasses: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentWillUnmount() {
|
||||
if (this.state.hidePlaceholderTimer) {
|
||||
clearTimeout(this.state.hidePlaceholderTimer);
|
||||
this.setState({hidePlaceholderTimer: null});
|
||||
}
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let tooltip;
|
||||
const tooltipBody = (
|
||||
this.props.mxEvent &&
|
||||
this.props.mxEvent.getContent() &&
|
||||
this.props.mxEvent.getContent().body) ?
|
||||
this.props.mxEvent.getContent().body : null;
|
||||
if (this.state.showTooltip && tooltipBody) {
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
tooltip = <RoomTooltip
|
||||
className='mx_RoleButton_tooltip'
|
||||
label={tooltipBody} />;
|
||||
}
|
||||
|
||||
const gutterSize = 0;
|
||||
let placeholderSize = 75;
|
||||
let placeholderFixupHeight = '100px';
|
||||
let placeholderTop = 0;
|
||||
let placeholderLeft = 0;
|
||||
|
||||
if (content.info) {
|
||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
||||
placeholderFixupHeight = content.info.h + 'px';
|
||||
}
|
||||
|
||||
placeholderSize = placeholderSize + 'px';
|
||||
|
||||
// Body 'ref' required by MImageBody
|
||||
return (
|
||||
<span className='mx_MStickerBody' ref='body'
|
||||
style={{
|
||||
height: placeholderFixupHeight,
|
||||
}}>
|
||||
<div className={'mx_MStickerBody_image_container'}>
|
||||
{ this.state.placeholderVisible &&
|
||||
<div
|
||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
||||
style={{
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
}}
|
||||
>
|
||||
<TintableSVG
|
||||
src={'img/icons-show-stickers.svg'}
|
||||
width={placeholderSize}
|
||||
height={placeholderSize} />
|
||||
</div> }
|
||||
<img
|
||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
||||
src={contentUrl}
|
||||
ref='image'
|
||||
alt={content.body}
|
||||
onLoad={this._onImageLoad}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
/>
|
||||
{ tooltip }
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty to prevent default behaviour of MImageBody
|
||||
onClick() {
|
||||
}
|
||||
}
|
|
@ -65,15 +65,19 @@ module.exports = React.createClass({
|
|||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
}
|
||||
|
||||
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
return <BodyType
|
||||
ref="body" mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -37,7 +37,15 @@ module.exports = React.createClass({
|
|||
displayName: 'AppsDrawer',
|
||||
|
||||
propTypes: {
|
||||
userId: PropTypes.string.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
showApps: PropTypes.bool, // Should apps be rendered
|
||||
hide: PropTypes.bool, // If rendered, should apps drawer be visible
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
showApps: true,
|
||||
hide: false,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -48,7 +56,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -58,7 +66,7 @@ module.exports = React.createClass({
|
|||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.log("Failed to connect to integrations server");
|
||||
console.log('Failed to connect to integrations server');
|
||||
// TODO -- Handle Scalar errors
|
||||
// this.setState({
|
||||
// scalar_error: err,
|
||||
|
@ -72,7 +80,7 @@ module.exports = React.createClass({
|
|||
componentWillUnmount: function() {
|
||||
ScalarMessaging.stopListening();
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
@ -83,7 +91,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAction: function(action) {
|
||||
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
|
@ -111,7 +119,7 @@ module.exports = React.createClass({
|
|||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { "$bar": "baz" }.
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
|
@ -192,13 +200,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_launchManageIntegrations: function() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
}, 'mx_IntegrationsManager');
|
||||
},
|
||||
|
||||
onClickAddWidget: function(e) {
|
||||
|
@ -206,12 +214,12 @@ module.exports = React.createClass({
|
|||
// Display a warning dialog if the max number of widgets have already been added to the room
|
||||
const apps = this._getApps();
|
||||
if (apps && apps.length >= MAX_WIDGETS) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
|
||||
console.error(errorMsg);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Cannot add any more widgets"),
|
||||
description: _t("The maximum permitted number of widgets have already been added to this room."),
|
||||
title: _t('Cannot add any more widgets'),
|
||||
description: _t('The maximum permitted number of widgets have already been added to this room.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -243,11 +251,11 @@ module.exports = React.createClass({
|
|||
) {
|
||||
addWidget = <div
|
||||
onClick={this.onClickAddWidget}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
className={this.state.apps.length<2 ?
|
||||
"mx_AddWidget_button mx_AddWidget_button_full_width" :
|
||||
"mx_AddWidget_button"
|
||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||
'mx_AddWidget_button'
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] { _t('Add a widget') }
|
||||
|
@ -255,8 +263,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_AppsDrawer">
|
||||
<div id="apps" className="mx_AppsContainer">
|
||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||
<div id='apps' className='mx_AppsContainer'>
|
||||
{ apps }
|
||||
</div>
|
||||
{ this._canUserModify() && addWidget }
|
||||
|
|
|
@ -32,7 +32,8 @@ module.exports = React.createClass({
|
|||
// js-sdk room object
|
||||
room: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
showApps: PropTypes.bool,
|
||||
showApps: PropTypes.bool, // Render apps
|
||||
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: PropTypes.object,
|
||||
|
@ -52,6 +53,11 @@ module.exports = React.createClass({
|
|||
onResize: PropTypes.func,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
showApps: true,
|
||||
hideAppsDrawer: false,
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
||||
!ObjectUtils.shallowEqual(this.state, nextState));
|
||||
|
@ -134,6 +140,7 @@ module.exports = React.createClass({
|
|||
userId={this.props.userId}
|
||||
maxHeight={this.props.maxHeight}
|
||||
showApps={this.props.showApps}
|
||||
hide={this.props.hideAppsDrawer}
|
||||
/>;
|
||||
|
||||
return (
|
||||
|
|
|
@ -36,6 +36,7 @@ const ObjectUtils = require('../../../ObjectUtils');
|
|||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.sticker': 'messages.MessageEvent',
|
||||
'm.call.invite': 'messages.TextualEvent',
|
||||
'm.call.answer': 'messages.TextualEvent',
|
||||
'm.call.hangup': 'messages.TextualEvent',
|
||||
|
@ -470,7 +471,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const eventType = this.props.mxEvent.getType();
|
||||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isInfoMessage = (eventType !== 'm.room.message');
|
||||
// For now assume that anything that doesn't have a content body is an isInfoMessage
|
||||
const isInfoMessage = !content.body; // Boolean comparison of non-boolean content body
|
||||
|
||||
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
|
|
|
@ -24,7 +24,7 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
|
@ -32,8 +32,6 @@ export default class MessageComposer extends React.Component {
|
|||
this.onCallClick = this.onCallClick.bind(this);
|
||||
this.onHangupClick = this.onHangupClick.bind(this);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
|
@ -202,20 +200,6 @@ export default class MessageComposer extends React.Component {
|
|||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onShowAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
}
|
||||
|
||||
onHideAppsClick(ev) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: false,
|
||||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
|
@ -281,7 +265,12 @@ export default class MessageComposer extends React.Component {
|
|||
alt={e2eTitle} title={e2eTitle}
|
||||
/>,
|
||||
);
|
||||
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||
|
||||
let callButton;
|
||||
let videoCallButton;
|
||||
let hangupButton;
|
||||
|
||||
// Call buttons
|
||||
if (this.props.callState && this.props.callState !== 'ended') {
|
||||
hangupButton =
|
||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
|
@ -298,19 +287,6 @@ export default class MessageComposer extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
// Apps
|
||||
if (this.props.showApps) {
|
||||
hideAppsButton =
|
||||
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||
<TintableSvg src="img/icons-hide-apps.svg" width="35" height="35" />
|
||||
</div>;
|
||||
} else {
|
||||
showAppsButton =
|
||||
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||
<TintableSvg src="img/icons-show-apps.svg" width="35" height="35" />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
MatrixClientPeg.get().credentials.userId);
|
||||
|
||||
|
@ -353,6 +329,11 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
let stickerpickerButton;
|
||||
if (SettingsStore.isFeatureEnabled('feature_sticker_messages')) {
|
||||
stickerpickerButton = <Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />;
|
||||
}
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
|
@ -364,12 +345,11 @@ export default class MessageComposer extends React.Component {
|
|||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
uploadButton,
|
||||
hangupButton,
|
||||
callButton,
|
||||
videoCallButton,
|
||||
showAppsButton,
|
||||
hideAppsButton,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
|
|
|
@ -392,7 +392,7 @@ module.exports = React.createClass({
|
|||
let manageIntegsButton;
|
||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||
manageIntegsButton = <ManageIntegsButton
|
||||
roomId={this.props.room.roomId}
|
||||
room={this.props.room}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
290
src/components/views/rooms/Stickerpicker.js
Normal file
290
src/components/views/rooms/Stickerpicker.js
Normal file
|
@ -0,0 +1,290 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Widgets from '../../../utils/widgets';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onShowStickersClick = this._onShowStickersClick.bind(this);
|
||||
this._onHideStickersClick = this._onHideStickersClick.bind(this);
|
||||
this._launchManageIntegrations = this._launchManageIntegrations.bind(this);
|
||||
this._removeStickerpickerWidgets = this._removeStickerpickerWidgets.bind(this);
|
||||
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||
this._onFinished = this._onFinished.bind(this);
|
||||
|
||||
this.popoverWidth = 300;
|
||||
this.popoverHeight = 300;
|
||||
|
||||
this.state = {
|
||||
showStickers: false,
|
||||
imError: null,
|
||||
};
|
||||
}
|
||||
|
||||
_removeStickerpickerWidgets() {
|
||||
console.warn('Removing Stickerpicker widgets');
|
||||
if (this.widgetId) {
|
||||
this.scalarClient.disableWidgetAssets(widgetType, this.widgetId).then(() => {
|
||||
console.warn('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
} else {
|
||||
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
|
||||
setTimeout(() => this.stickersMenu.close());
|
||||
Widgets.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.error('Failed to remove sticker picker widget', e);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.scalarClient = null;
|
||||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||
this.scalarClient = new ScalarAuthClient();
|
||||
this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
this._imError("Failed to connect to integrations server", e);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.state.imError) {
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
}
|
||||
|
||||
_imError(errorMsg, e) {
|
||||
console.error(errorMsg, e);
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
imError: errorMsg,
|
||||
});
|
||||
}
|
||||
|
||||
_onWidgetAction(payload) {
|
||||
if (payload.action === "user_widget_updated") {
|
||||
this.forceUpdate();
|
||||
} else if (payload.action === "stickerpicker_close") {
|
||||
// Wrap this in a timeout in order to avoid the DOM node from being
|
||||
// pulled from under its feet
|
||||
setTimeout(() => this.stickersMenu.close());
|
||||
}
|
||||
}
|
||||
|
||||
_defaultStickerpickerContent() {
|
||||
return (
|
||||
<AccessibleButton onClick={this._launchManageIntegrations}
|
||||
className='mx_Stickers_contentPlaceholder'>
|
||||
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
|
||||
<p className='mx_Stickers_addLink'>Add some now</p>
|
||||
<img src='img/stickerpack-placeholder.png' alt={_t('Add a stickerpack')} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
_errorStickerpickerContent() {
|
||||
return (
|
||||
<div style={{"text-align": "center"}} className="error">
|
||||
<p> { this.state.imError } </p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_getStickerpickerContent() {
|
||||
// Handle Integration Manager errors
|
||||
if (this.state._imError) {
|
||||
return this._errorStickerpickerContent();
|
||||
}
|
||||
|
||||
// Stickers
|
||||
// TODO - Add support for Stickerpickers from multiple app stores.
|
||||
// Render content from multiple stickerpack sources, each within their
|
||||
// own iframe, within the stickerpicker UI element.
|
||||
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
|
||||
let stickersContent;
|
||||
|
||||
// Load stickerpack content
|
||||
if (stickerpickerWidget && stickerpickerWidget.content && stickerpickerWidget.content.url) {
|
||||
// Set default name
|
||||
stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");
|
||||
this.widgetId = stickerpickerWidget.id;
|
||||
|
||||
stickersContent = (
|
||||
<div className='mx_Stickers_content_container'>
|
||||
<div
|
||||
id='stickersContent'
|
||||
className='mx_Stickers_content'
|
||||
style={{
|
||||
border: 'none',
|
||||
height: this.popoverHeight,
|
||||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<AppTile
|
||||
id={stickerpickerWidget.id}
|
||||
url={stickerpickerWidget.content.url}
|
||||
name={stickerpickerWidget.content.name}
|
||||
room={this.props.room}
|
||||
type={stickerpickerWidget.content.type}
|
||||
fullWidth={true}
|
||||
userId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={MatrixClientPeg.get().credentials.userId}
|
||||
waitForIframeLoad={true}
|
||||
show={true}
|
||||
showMenubar={true}
|
||||
onEditClick={this._launchManageIntegrations}
|
||||
onDeleteClick={this._removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showMinimise={true}
|
||||
showDelete={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
handleMinimisePointerEvents={true}
|
||||
whitelistCapabilities={['m.sticker']}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Default content to show if stickerpicker widget not added
|
||||
console.warn("No available sticker picker widgets");
|
||||
stickersContent = this._defaultStickerpickerContent();
|
||||
this.widgetId = null;
|
||||
this.forceUpdate();
|
||||
}
|
||||
this.setState({
|
||||
showStickers: false,
|
||||
});
|
||||
return stickersContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the sticker picker overlay
|
||||
* If no stickerpacks have been added, show a link to the integration manager add sticker packs page.
|
||||
* @param {Event} e Event that triggered the function
|
||||
*/
|
||||
_onShowStickersClick(e) {
|
||||
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset - 42;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
// const self = this;
|
||||
this.stickersMenu = ContextualMenu.createMenu(GenericElementContextMenu, {
|
||||
chevronOffset: 10,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: this.popoverWidth,
|
||||
menuHeight: this.popoverHeight,
|
||||
element: this._getStickerpickerContent(),
|
||||
onFinished: this._onFinished,
|
||||
menuPaddingTop: 0,
|
||||
});
|
||||
|
||||
|
||||
this.setState({showStickers: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger hiding of the sticker picker overlay
|
||||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
_onHideStickersClick(ev) {
|
||||
setTimeout(() => this.stickersMenu.close());
|
||||
}
|
||||
|
||||
/**
|
||||
* The stickers picker was hidden
|
||||
*/
|
||||
_onFinished() {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the integrations manager on the stickers integration page
|
||||
*/
|
||||
_launchManageIntegrations() {
|
||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||
this.scalarClient.getScalarInterfaceUrlForRoom(
|
||||
this.props.room,
|
||||
'type_' + widgetType,
|
||||
this.widgetId,
|
||||
) :
|
||||
null;
|
||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||
src: src,
|
||||
}, "mx_IntegrationsManager");
|
||||
|
||||
// Wrap this in a timeout in order to avoid the DOM node from being pulled from under its feet
|
||||
setTimeout(() => this.stickersMenu.close());
|
||||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
let stickersButton;
|
||||
if (this.state.showStickers) {
|
||||
// Show hide-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
id='stickersButton'
|
||||
key="controls_hide_stickers"
|
||||
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
|
||||
onClick={this._onHideStickersClick}
|
||||
ref='target'
|
||||
title={_t("Hide Stickers")}>
|
||||
<TintableSvg src="img/icons-hide-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
} else {
|
||||
// Show show-stickers button
|
||||
stickersButton =
|
||||
<div
|
||||
id='stickersButton'
|
||||
key="constrols_show_stickers"
|
||||
className="mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}>
|
||||
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
|
||||
</div>;
|
||||
}
|
||||
return stickersButton;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue