Merge branch 'develop' into release-v0.12.4

This commit is contained in:
Luke Barnard 2018-05-14 17:43:40 +01:00
commit e596924074
14 changed files with 65 additions and 32 deletions

View file

@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ReplyThread {
margin-top: 0;
}
.mx_ReplyThread .mx_DateSeparator { .mx_ReplyThread .mx_DateSeparator {
font-size: 1em !important; font-size: 1em !important;
margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 1px; padding-bottom: 1px;
bottom: -5px; bottom: -5px;

View file

@ -116,6 +116,12 @@ export default class FromWidgetPostMessageApi {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise 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 action = event.data.action;
const widgetId = event.data.widgetId; const widgetId = event.data.widgetId;
if (action === 'content_loaded') { if (action === 'content_loaded') {
@ -137,12 +143,15 @@ export default class FromWidgetPostMessageApi {
}); });
} else if (action === 'm.sticker') { } else if (action === 'm.sticker') {
// console.warn('Got sticker message from widget', widgetId); // 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') { } else if (action === 'integration_manager_open') {
// Close the stickerpicker // Close the stickerpicker
dis.dispatch({action: 'stickerpicker_close'}); dis.dispatch({action: 'stickerpicker_close'});
// Open the integration manager // 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 integType = (data && data.integType) ? data.integType : null;
const integId = (data && data.integId) ? data.integId : null; const integId = (data && data.integId) ? data.integId : null;
IntegrationManager.open(integType, integId); IntegrationManager.open(integType, integId);

View file

@ -186,7 +186,6 @@ const sanitizeHtmlParams = {
], ],
allowedAttributes: { allowedAttributes: {
// custom ones first: // 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 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 span: ['data-mx-bg-color', 'data-mx-color', 'style'], // custom to matrix
a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix a: ['href', 'name', 'target', 'rel'], // remote target: custom to matrix

View file

@ -349,7 +349,7 @@ function setWidget(event, roomId) {
userWidgets[widgetId] = { userWidgets[widgetId] = {
content: content, content: content,
sender: client.getUserId(), sender: client.getUserId(),
stateKey: widgetId, state_key: widgetId,
type: 'm.widget', type: 'm.widget',
id: widgetId, id: widgetId,
}; };

View file

@ -51,11 +51,11 @@ export default class ToWidgetPostMessageApi {
if (payload.response === undefined) { if (payload.response === undefined) {
return; return;
} }
const promise = this._requestMap[payload._id]; const promise = this._requestMap[payload.requestId];
if (!promise) { if (!promise) {
return; return;
} }
delete this._requestMap[payload._id]; delete this._requestMap[payload.requestId];
promise.resolve(payload); promise.resolve(payload);
} }
@ -64,21 +64,21 @@ export default class ToWidgetPostMessageApi {
targetWindow = targetWindow || window.parent; // default to parent window targetWindow = targetWindow || window.parent; // default to parent window
targetOrigin = targetOrigin || "*"; targetOrigin = targetOrigin || "*";
this._counter += 1; 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) => { return new Promise((resolve, reject) => {
this._requestMap[action._id] = {resolve, reject}; this._requestMap[action.requestId] = {resolve, reject};
targetWindow.postMessage(action, targetOrigin); targetWindow.postMessage(action, targetOrigin);
if (this._timeoutMs > 0) { if (this._timeoutMs > 0) {
setTimeout(() => { setTimeout(() => {
if (!this._requestMap[action._id]) { if (!this._requestMap[action.requestId]) {
return; return;
} }
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action), console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
this._requestMap); this._requestMap);
this._requestMap[action._id].reject(new Error("Timed out")); this._requestMap[action.requestId].reject(new Error("Timed out"));
delete this._requestMap[action._id]; delete this._requestMap[action.requestId];
}, this._timeoutMs); }, this._timeoutMs);
} }
}); });

View file

@ -44,6 +44,8 @@ export default class WidgetMessaging {
} }
messageToWidget(action) { messageToWidget(action) {
action.widgetId = this.widgetId; // Required to be sent for all outbound requests
return this.toWidget.exec(action, this.target).then((data) => { return this.toWidget.exec(action, this.target).then((data) => {
// Check for errors and reject if found // Check for errors and reject if found
if (data.response === undefined) { // null is valid if (data.response === undefined) { // null is valid

View file

@ -80,6 +80,7 @@ const SIMPLE_SETTINGS = [
{ id: "TextualBody.disableBigEmoji" }, { id: "TextualBody.disableBigEmoji" },
{ id: "VideoView.flipVideoHorizontally" }, { id: "VideoView.flipVideoHorizontally" },
{ id: "TagPanel.disableTagPanel" }, { id: "TagPanel.disableTagPanel" },
{ id: "enableWidgetScreenshots" },
]; ];
// These settings must be defined in SettingsStore // These settings must be defined in SettingsStore

View file

@ -85,7 +85,7 @@ export default class AppTile extends React.Component {
/** /**
* Does the widget support a given capability * 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 * @return {Boolean} True if capability supported
*/ */
_hasCapability(capability) { _hasCapability(capability) {
@ -281,6 +281,11 @@ export default class AppTile extends React.Component {
} }
_canUserModify() { _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); 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. // 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 showPictureSnapshotIcon = 'img/camera_green.svg';
const popoutWidgetIcon = 'img/button-new-window.svg'; const popoutWidgetIcon = 'img/button-new-window.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg'); const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
@ -702,13 +707,15 @@ AppTile.propTypes = {
showDelete: PropTypes.bool, showDelete: PropTypes.bool,
// Optionally hide the popout widget icon // Optionally hide the popout widget icon
showPopout: PropTypes.bool, 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 // NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events. // basic widget capabilities, e.g. injecting sticker message events.
whitelistCapabilities: PropTypes.array, whitelistCapabilities: PropTypes.array,
// Optional function to be called on widget capability request // Optional function to be called on widget capability request
// Called with an array of the requested capabilities // Called with an array of the requested capabilities
onCapabilityRequest: PropTypes.func, onCapabilityRequest: PropTypes.func,
// Is this an instance of a user widget
userWidget: PropTypes.bool,
}; };
AppTile.defaultProps = { AppTile.defaultProps = {
@ -721,4 +728,5 @@ AppTile.defaultProps = {
showPopout: true, showPopout: true,
handleMinimisePointerEvents: false, handleMinimisePointerEvents: false,
whitelistCapabilities: [], whitelistCapabilities: [],
userWidget: false,
}; };

View file

@ -81,7 +81,7 @@ export default class ReplyThread extends React.Component {
// Part of Replies fallback support // Part of Replies fallback support
static stripHTMLReply(html) { static stripHTMLReply(html) {
return html.replace(/^<blockquote data-mx-reply>[\s\S]+?<!--end-mx-reply--><\/blockquote>/, ''); return html.replace(/^<mx-reply>[\s\S]+?<\/mx-reply>/, '');
} }
// Part of Replies fallback support // Part of Replies fallback support
@ -102,8 +102,8 @@ export default class ReplyThread extends React.Component {
switch (ev.getContent().msgtype) { switch (ev.getContent().msgtype) {
case 'm.text': case 'm.text':
case 'm.notice': { case 'm.notice': {
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>${html || body}<!--end-mx-reply--></blockquote>`; + `<br>${html || body}</blockquote></mx-reply>`;
const lines = body.trim().split('\n'); const lines = body.trim().split('\n');
if (lines.length > 0) { if (lines.length > 0) {
lines[0] = `<${mxid}> ${lines[0]}`; lines[0] = `<${mxid}> ${lines[0]}`;
@ -112,28 +112,28 @@ export default class ReplyThread extends React.Component {
break; break;
} }
case 'm.image': case 'm.image':
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>sent an image.<!--end-mx-reply--></blockquote>`; + `<br>sent an image.</blockquote></mx-reply>`;
body = `> <${mxid}> sent an image.\n\n`; body = `> <${mxid}> sent an image.\n\n`;
break; break;
case 'm.video': case 'm.video':
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>sent a video.<!--end-mx-reply--></blockquote>`; + `<br>sent a video.</blockquote></mx-reply>`;
body = `> <${mxid}> sent a video.\n\n`; body = `> <${mxid}> sent a video.\n\n`;
break; break;
case 'm.audio': case 'm.audio':
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>sent an audio file.<!--end-mx-reply--></blockquote>`; + `<br>sent an audio file.</blockquote></mx-reply>`;
body = `> <${mxid}> sent an audio file.\n\n`; body = `> <${mxid}> sent an audio file.\n\n`;
break; break;
case 'm.file': case 'm.file':
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>sent a file.<!--end-mx-reply--></blockquote>`; + `<br>sent a file.</blockquote></mx-reply>`;
body = `> <${mxid}> sent a file.\n\n`; body = `> <${mxid}> sent a file.\n\n`;
break; break;
case 'm.emote': { case 'm.emote': {
html = `<blockquote data-mx-reply><a href="${evLink}">In reply to</a> * ` html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
+ `<a href="${userLink}">${mxid}</a><br>${html || body}<!--end-mx-reply--></blockquote>`; + `<a href="${userLink}">${mxid}</a><br>${html || body}</blockquote></mx-reply>`;
const lines = body.trim().split('\n'); const lines = body.trim().split('\n');
if (lines.length > 0) { if (lines.length > 0) {
lines[0] = `* <${mxid}> ${lines[0]}`; lines[0] = `* <${mxid}> ${lines[0]}`;

View file

@ -227,6 +227,8 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
const apps = this.state.apps.map( const apps = this.state.apps.map(
(app, index, arr) => { (app, index, arr) => {
return (<AppTile return (<AppTile
@ -242,6 +244,7 @@ module.exports = React.createClass({
creatorUserId={app.creatorUserId} creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''} widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
waitForIframeLoad={app.waitForIframeLoad} waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={enableScreenshots ? ["m.capability.screenshot"] : []}
/>); />);
}); });

View file

@ -220,8 +220,8 @@ export default class Stickerpicker extends React.Component {
room={this.props.room} room={this.props.room}
type={stickerpickerWidget.content.type} type={stickerpickerWidget.content.type}
fullWidth={true} fullWidth={true}
userId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId} userId={MatrixClientPeg.get().credentials.userId}
creatorUserId={MatrixClientPeg.get().credentials.userId} creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
waitForIframeLoad={true} waitForIframeLoad={true}
show={true} show={true}
showMenubar={true} showMenubar={true}
@ -234,6 +234,7 @@ export default class Stickerpicker extends React.Component {
onMinimiseClick={this._onHideStickersClick} onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
whitelistCapabilities={['m.sticker', 'visibility']} whitelistCapabilities={['m.sticker', 'visibility']}
userWidget={true}
/> />
</PersistedElement> </PersistedElement>
</div> </div>

View file

@ -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 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", "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
"Room Colour": "Room Colour", "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 app version information": "Collecting app version information",
"Collecting logs": "Collecting logs", "Collecting logs": "Collecting logs",
"Uploading report": "Uploading report", "Uploading report": "Uploading report",

View file

@ -265,4 +265,9 @@ export const SETTINGS = {
default: true, default: true,
controller: new AudioNotificationsEnabledController(), controller: new AudioNotificationsEnabledController(),
}, },
"enableWidgetScreenshots": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable widget screenshots on supported widgets'),
default: false,
},
}; };

View file

@ -58,8 +58,7 @@ function getUserWidgetsArray() {
*/ */
function getStickerpickerWidgets() { function getStickerpickerWidgets() {
const widgets = getUserWidgetsArray(); const widgets = getUserWidgetsArray();
const stickerpickerWidgets = widgets.filter((widget) => widget.type='m.stickerpicker'); return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker");
return stickerpickerWidgets;
} }
/** /**
@ -73,7 +72,7 @@ function removeStickerpickerWidgets() {
} }
const userWidgets = client.getAccountData('m.widgets').getContent() || {}; const userWidgets = client.getAccountData('m.widgets').getContent() || {};
Object.entries(userWidgets).forEach(([key, widget]) => { Object.entries(userWidgets).forEach(([key, widget]) => {
if (widget.type === 'm.stickerpicker') { if (widget.content && widget.content.type === 'm.stickerpicker') {
delete userWidgets[key]; delete userWidgets[key];
} }
}); });