diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index a02f42751c..bf44a11728 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -14,8 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_ReplyThread { + margin-top: 0; +} + .mx_ReplyThread .mx_DateSeparator { font-size: 1em !important; + margin-top: 0; margin-bottom: 0; padding-bottom: 1px; bottom: -5px; diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index ad1f1acbbd..792fd73733 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -116,6 +116,12 @@ export default class FromWidgetPostMessageApi { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } + // Although the requestId is required, we don't use it. We'll be nice and process the message + // if the property is missing, but with a warning for widget developers. + if (!event.data.requestId) { + console.warn("fromWidget action '" + event.data.action + "' does not have a requestId"); + } + const action = event.data.action; const widgetId = event.data.widgetId; if (action === 'content_loaded') { @@ -137,12 +143,15 @@ export default class FromWidgetPostMessageApi { }); } else if (action === 'm.sticker') { // console.warn('Got sticker message from widget', widgetId); - dis.dispatch({action: 'm.sticker', data: event.data.widgetData, widgetId: event.data.widgetId}); + // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually + const data = event.data.data || event.data.widgetData; + dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId}); } else if (action === 'integration_manager_open') { // Close the stickerpicker dis.dispatch({action: 'stickerpicker_close'}); // Open the integration manager - const data = event.data.widgetData; + // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually + const data = event.data.data || event.data.widgetData; const integType = (data && data.integType) ? data.integType : null; const integId = (data && data.integId) ? data.integId : null; IntegrationManager.open(integType, integId); diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 82b6830b78..58572cf144 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -186,7 +186,6 @@ const sanitizeHtmlParams = { ], allowedAttributes: { // custom ones first: - blockquote: ['data-mx-reply'], // used to allow explicit removal of a reply fallback blockquote, value ignored font: ['color', 'data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 123d02159e..a163bf7bbd 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -349,7 +349,7 @@ function setWidget(event, roomId) { userWidgets[widgetId] = { content: content, sender: client.getUserId(), - stateKey: widgetId, + state_key: widgetId, type: 'm.widget', id: widgetId, }; diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js index ccaa0207c1..def4af56ae 100644 --- a/src/ToWidgetPostMessageApi.js +++ b/src/ToWidgetPostMessageApi.js @@ -51,11 +51,11 @@ export default class ToWidgetPostMessageApi { if (payload.response === undefined) { return; } - const promise = this._requestMap[payload._id]; + const promise = this._requestMap[payload.requestId]; if (!promise) { return; } - delete this._requestMap[payload._id]; + delete this._requestMap[payload.requestId]; promise.resolve(payload); } @@ -64,21 +64,21 @@ export default class ToWidgetPostMessageApi { targetWindow = targetWindow || window.parent; // default to parent window targetOrigin = targetOrigin || "*"; this._counter += 1; - action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter; + action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter; return new Promise((resolve, reject) => { - this._requestMap[action._id] = {resolve, reject}; + this._requestMap[action.requestId] = {resolve, reject}; targetWindow.postMessage(action, targetOrigin); if (this._timeoutMs > 0) { setTimeout(() => { - if (!this._requestMap[action._id]) { + if (!this._requestMap[action.requestId]) { return; } console.error("postMessage request timed out. Sent object: " + JSON.stringify(action), this._requestMap); - this._requestMap[action._id].reject(new Error("Timed out")); - delete this._requestMap[action._id]; + this._requestMap[action.requestId].reject(new Error("Timed out")); + delete this._requestMap[action.requestId]; }, this._timeoutMs); } }); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 86eaa0b59b..5b722df65f 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -44,6 +44,8 @@ export default class WidgetMessaging { } messageToWidget(action) { + action.widgetId = this.widgetId; // Required to be sent for all outbound requests + return this.toWidget.exec(action, this.target).then((data) => { // Check for errors and reject if found if (data.response === undefined) { // null is valid diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 3bef4e41bb..35a55284fd 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -80,6 +80,7 @@ const SIMPLE_SETTINGS = [ { id: "TextualBody.disableBigEmoji" }, { id: "VideoView.flipVideoHorizontally" }, { id: "TagPanel.disableTagPanel" }, + { id: "enableWidgetScreenshots" }, ]; // These settings must be defined in SettingsStore diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 55e2b93920..0895ede636 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -85,7 +85,7 @@ export default class AppTile extends React.Component { /** * Does the widget support a given capability - * @param {[type]} capability Capability to check for + * @param {string} capability Capability to check for * @return {Boolean} True if capability supported */ _hasCapability(capability) { @@ -281,6 +281,11 @@ export default class AppTile extends React.Component { } _canUserModify() { + // User widgets should always be modifiable by their creator + if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { + return true; + } + // Check if the current user can modify widgets in the current room return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); } @@ -598,7 +603,7 @@ export default class AppTile extends React.Component { } // Picture snapshot - only show button when apps are maximised. - const showPictureSnapshotButton = this._hasCapability('screenshot') && this.props.show; + const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show; const showPictureSnapshotIcon = 'img/camera_green.svg'; const popoutWidgetIcon = 'img/button-new-window.svg'; const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg'); @@ -702,13 +707,15 @@ AppTile.propTypes = { showDelete: PropTypes.bool, // Optionally hide the popout widget icon showPopout: PropTypes.bool, - // Widget apabilities to allow by default (without user confirmation) + // Widget capabilities 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, + // Is this an instance of a user widget + userWidget: PropTypes.bool, }; AppTile.defaultProps = { @@ -721,4 +728,5 @@ AppTile.defaultProps = { showPopout: true, handleMinimisePointerEvents: false, whitelistCapabilities: [], + userWidget: false, }; diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index cd444fb090..6714de81a4 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -81,7 +81,7 @@ export default class ReplyThread extends React.Component { // Part of Replies fallback support static stripHTMLReply(html) { - return html.replace(/^
[\s\S]+?<\/blockquote>/, ''); + return html.replace(/^[\s\S]+?<\/mx-reply>/, ''); } // Part of Replies fallback support @@ -102,8 +102,8 @@ export default class ReplyThread extends React.Component { switch (ev.getContent().msgtype) { case 'm.text': case 'm.notice': { - html = ` In reply to ${mxid}` - + ``; + html = `
${html || body}`; const lines = body.trim().split('\n'); if (lines.length > 0) { lines[0] = `<${mxid}> ${lines[0]}`; @@ -112,28 +112,28 @@ export default class ReplyThread extends React.Component { break; } case 'm.image': - html = ` In reply to ${mxid}` + + `
${html || body}In reply to ${mxid}` - + ``; + html = `
sent an image.`; body = `> <${mxid}> sent an image.\n\n`; break; case 'm.video': - html = ` In reply to ${mxid}` + + `
sent an image.In reply to ${mxid}` - + ``; + html = `
sent a video.`; body = `> <${mxid}> sent a video.\n\n`; break; case 'm.audio': - html = ` In reply to ${mxid}` + + `
sent a video.In reply to ${mxid}` - + ``; + html = `
sent an audio file.`; body = `> <${mxid}> sent an audio file.\n\n`; break; case 'm.file': - html = ` In reply to ${mxid}` + + `
sent an audio file.In reply to ${mxid}` - + ``; + html = `
sent a file.`; body = `> <${mxid}> sent a file.\n\n`; break; case 'm.emote': { - html = ` In reply to ${mxid}` + + `
sent a file.In reply to * ` - + `${mxid}`; + html = `
${html || body}`; const lines = body.trim().split('\n'); if (lines.length > 0) { lines[0] = `* <${mxid}> ${lines[0]}`; diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 9f57ca51e9..8763ea3d7f 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -227,6 +227,8 @@ module.exports = React.createClass({ }, render: function() { + const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id); + const apps = this.state.apps.map( (app, index, arr) => { return ( In reply to * ` + + `${mxid}
${html || body}); }); diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 8f9e96dd3b..6152809c1a 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -220,8 +220,8 @@ export default class Stickerpicker extends React.Component { room={this.props.room} type={stickerpickerWidget.content.type} fullWidth={true} - userId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} - creatorUserId={MatrixClientPeg.get().credentials.userId} + userId={MatrixClientPeg.get().credentials.userId} + creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} waitForIframeLoad={true} show={true} showMenubar={true} @@ -234,6 +234,7 @@ export default class Stickerpicker extends React.Component { onMinimiseClick={this._onHideStickersClick} handleMinimisePointerEvents={true} whitelistCapabilities={['m.sticker', 'visibility']} + userWidget={true} /> diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4adca0cc72..9b932ef2b6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -217,6 +217,7 @@ "Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)", "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room", "Room Colour": "Room Colour", + "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 89a12580d6..663318f990 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -265,4 +265,9 @@ export const SETTINGS = { default: true, controller: new AudioNotificationsEnabledController(), }, + "enableWidgetScreenshots": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Enable widget screenshots on supported widgets'), + default: false, + }, }; diff --git a/src/utils/widgets.js b/src/utils/widgets.js index 0d7f5dbf3f..338df184e2 100644 --- a/src/utils/widgets.js +++ b/src/utils/widgets.js @@ -58,8 +58,7 @@ function getUserWidgetsArray() { */ function getStickerpickerWidgets() { const widgets = getUserWidgetsArray(); - const stickerpickerWidgets = widgets.filter((widget) => widget.type='m.stickerpicker'); - return stickerpickerWidgets; + return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker"); } /** @@ -73,7 +72,7 @@ function removeStickerpickerWidgets() { } const userWidgets = client.getAccountData('m.widgets').getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.type === 'm.stickerpicker') { + if (widget.content && widget.content.type === 'm.stickerpicker') { delete userWidgets[key]; } });