Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
3d3fd35815
27 changed files with 1650 additions and 556 deletions
|
@ -275,6 +275,13 @@ class ContentMessages {
|
||||||
this.nextId = 0;
|
this.nextId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
|
||||||
|
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sendContentToRoom(file, roomId, matrixClient) {
|
sendContentToRoom(file, roomId, matrixClient) {
|
||||||
const content = {
|
const content = {
|
||||||
body: file.name || 'Attachment',
|
body: file.name || 'Attachment',
|
||||||
|
|
201
src/FromWidgetPostMessageApi.js
Normal file
201
src/FromWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
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 URL from 'url';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import IntegrationManager from './IntegrationManager';
|
||||||
|
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||||
|
|
||||||
|
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
||||||
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
'0.0.1',
|
||||||
|
];
|
||||||
|
const INBOUND_API_NAME = 'fromWidget';
|
||||||
|
|
||||||
|
// Listen for and handle incomming requests using the 'fromWidget' postMessage
|
||||||
|
// API and initiate responses
|
||||||
|
export default class FromWidgetPostMessageApi {
|
||||||
|
constructor() {
|
||||||
|
this.widgetMessagingEndpoints = [];
|
||||||
|
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a widget endpoint for trusted postMessage communication
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
*/
|
||||||
|
addEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
||||||
|
if (this.widgetMessagingEndpoints.some(function(ep) {
|
||||||
|
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||||
|
})) {
|
||||||
|
// Message endpoint already registered
|
||||||
|
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.warn(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
||||||
|
this.widgetMessagingEndpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* De-register a widget endpoint from trusted communication sources
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||||
|
* @return {boolean} True if endpoint was successfully removed
|
||||||
|
*/
|
||||||
|
removeEndpoint(widgetId, endpointUrl) {
|
||||||
|
const u = URL.parse(endpointUrl);
|
||||||
|
if (!u || !u.protocol || !u.host) {
|
||||||
|
console.warn('Remove widget messaging endpoint - Invalid origin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = u.protocol + '//' + u.host;
|
||||||
|
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
||||||
|
const length = this.widgetMessagingEndpoints.length;
|
||||||
|
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints.
|
||||||
|
filter(function(endpoint) {
|
||||||
|
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
||||||
|
});
|
||||||
|
return (length > this.widgetMessagingEndpoints.length);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle widget postMessage events
|
||||||
|
* Messages are only handled where a valid, registered messaging endpoints
|
||||||
|
* @param {Event} event Event to handle
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
onPostMessage(event) {
|
||||||
|
if (!event.origin) { // Handle chrome
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event origin is empty string if undefined
|
||||||
|
if (
|
||||||
|
event.origin.length === 0 ||
|
||||||
|
!this.trustedEndpoint(event.origin) ||
|
||||||
|
event.data.api !== INBOUND_API_NAME ||
|
||||||
|
!event.data.widgetId
|
||||||
|
) {
|
||||||
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = event.data.action;
|
||||||
|
const widgetId = event.data.widgetId;
|
||||||
|
if (action === 'content_loaded') {
|
||||||
|
console.warn('Widget reported content loaded for', widgetId);
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'widget_content_loaded',
|
||||||
|
widgetId: widgetId,
|
||||||
|
});
|
||||||
|
this.sendResponse(event, {success: true});
|
||||||
|
} else if (action === 'supported_api_versions') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||||
|
});
|
||||||
|
} else if (action === 'api_version') {
|
||||||
|
this.sendResponse(event, {
|
||||||
|
api: INBOUND_API_NAME,
|
||||||
|
version: WIDGET_API_VERSION,
|
||||||
|
});
|
||||||
|
} 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});
|
||||||
|
} else if (action === 'integration_manager_open') {
|
||||||
|
// Close the stickerpicker
|
||||||
|
dis.dispatch({action: 'stickerpicker_close'});
|
||||||
|
// Open the integration manager
|
||||||
|
const data = event.data.widgetData;
|
||||||
|
const integType = (data && data.integType) ? data.integType : null;
|
||||||
|
const integId = (data && data.integId) ? data.integId : null;
|
||||||
|
IntegrationManager.open(integType, integId);
|
||||||
|
} else {
|
||||||
|
console.warn('Widget postMessage event unhandled');
|
||||||
|
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if message origin is registered as trusted
|
||||||
|
* @param {string} origin PostMessage origin to check
|
||||||
|
* @return {boolean} True if trusted
|
||||||
|
*/
|
||||||
|
trustedEndpoint(origin) {
|
||||||
|
if (!origin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.widgetMessagingEndpoints.some((endpoint) => {
|
||||||
|
// TODO / FIXME -- Should this also check the widgetId?
|
||||||
|
return endpoint.endpointUrl === origin;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a postmessage response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {Object} res Response data
|
||||||
|
*/
|
||||||
|
sendResponse(event, res) {
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = res;
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an error response to a postMessage request
|
||||||
|
* @param {Event} event The original postMessage request event
|
||||||
|
* @param {string} msg Error message
|
||||||
|
* @param {Error} nestedError Nested error event (optional)
|
||||||
|
*/
|
||||||
|
sendError(event, msg, nestedError) {
|
||||||
|
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = {
|
||||||
|
error: {
|
||||||
|
message: msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (nestedError) {
|
||||||
|
data.response.error._error = nestedError;
|
||||||
|
}
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
}
|
73
src/IntegrationManager.js
Normal file
73
src/IntegrationManager.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 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 Modal from './Modal';
|
||||||
|
import sdk from './index';
|
||||||
|
import SdkConfig from './SdkConfig';
|
||||||
|
import ScalarMessaging from './ScalarMessaging';
|
||||||
|
import ScalarAuthClient from './ScalarAuthClient';
|
||||||
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
|
|
||||||
|
if (!global.mxIntegrationManager) {
|
||||||
|
global.mxIntegrationManager = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IntegrationManager {
|
||||||
|
static _init() {
|
||||||
|
if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) {
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
|
global.mxIntegrationManager.client = new ScalarAuthClient();
|
||||||
|
|
||||||
|
return global.mxIntegrationManager.client.connect().then(() => {
|
||||||
|
global.mxIntegrationManager.connected = true;
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("Failed to connect to integrations server", e);
|
||||||
|
global.mxIntegrationManager.error = e;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Invalid integration manager config', SdkConfig.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the integrations manager on the stickers integration page
|
||||||
|
* @param {string} integName integration / widget type
|
||||||
|
* @param {string} integId integration / widget ID
|
||||||
|
* @param {function} onFinished Callback to invoke on integration manager close
|
||||||
|
*/
|
||||||
|
static async open(integName, integId, onFinished) {
|
||||||
|
await IntegrationManager._init();
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
if (global.mxIntegrationManager.error ||
|
||||||
|
!(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) {
|
||||||
|
console.error("Scalar error", global.mxIntegrationManager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const integType = 'type_' + integName;
|
||||||
|
const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ?
|
||||||
|
global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom(
|
||||||
|
{roomId: RoomViewStore.getRoomId()},
|
||||||
|
integType,
|
||||||
|
integId,
|
||||||
|
) :
|
||||||
|
null;
|
||||||
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
onFinished: onFinished,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,10 +148,48 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
/**
|
||||||
|
* Mark all assets associated with the specified widget as "disabled" in the
|
||||||
|
* integration manager database.
|
||||||
|
* This can be useful to temporarily prevent purchased assets from being displayed.
|
||||||
|
* @param {string} widgetType [description]
|
||||||
|
* @param {string} widgetId [description]
|
||||||
|
* @return {Promise} Resolves on completion
|
||||||
|
*/
|
||||||
|
disableWidgetAssets(widgetType, widgetId) {
|
||||||
|
let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state';
|
||||||
|
url = this.getStarterLink(url);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request({
|
||||||
|
method: 'GET',
|
||||||
|
uri: url,
|
||||||
|
json: true,
|
||||||
|
qs: {
|
||||||
|
'widget_type': widgetType,
|
||||||
|
'widget_id': widgetId,
|
||||||
|
'state': 'disable',
|
||||||
|
},
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else if (response.statusCode / 100 !== 2) {
|
||||||
|
reject({statusCode: response.statusCode});
|
||||||
|
} else if (!body) {
|
||||||
|
reject(new Error("Failed to set widget assets state"));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getScalarInterfaceUrlForRoom(room, screen, id) {
|
||||||
|
const roomId = room.roomId;
|
||||||
|
const roomName = room.name;
|
||||||
let url = SdkConfig.get().integrations_ui_url;
|
let url = SdkConfig.get().integrations_ui_url;
|
||||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||||
url += "&room_id=" + encodeURIComponent(roomId);
|
url += "&room_id=" + encodeURIComponent(roomId);
|
||||||
|
url += "&room_name=" + encodeURIComponent(roomName);
|
||||||
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
||||||
if (id) {
|
if (id) {
|
||||||
url += '&integ_id=' + encodeURIComponent(id);
|
url += '&integ_id=' + encodeURIComponent(id);
|
||||||
|
|
|
@ -235,6 +235,7 @@ const SdkConfig = require('./SdkConfig');
|
||||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
||||||
const dis = require("./dispatcher");
|
const dis = require("./dispatcher");
|
||||||
|
const Widgets = require('./utils/widgets');
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
|
@ -291,6 +292,7 @@ function setWidget(event, roomId) {
|
||||||
const widgetUrl = event.data.url;
|
const widgetUrl = event.data.url;
|
||||||
const widgetName = event.data.name; // optional
|
const widgetName = event.data.name; // optional
|
||||||
const widgetData = event.data.data; // optional
|
const widgetData = event.data.data; // optional
|
||||||
|
const userWidget = event.data.userWidget;
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -330,10 +332,46 @@ function setWidget(event, roomId) {
|
||||||
name: widgetName,
|
name: widgetName,
|
||||||
data: widgetData,
|
data: widgetData,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (userWidget) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const userWidgets = Widgets.getUserWidgets();
|
||||||
|
|
||||||
|
// Delete existing widget with ID
|
||||||
|
try {
|
||||||
|
delete userWidgets[widgetId];
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`$widgetId is non-configurable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new widget / update
|
||||||
|
if (widgetUrl !== null) {
|
||||||
|
userWidgets[widgetId] = {
|
||||||
|
content: content,
|
||||||
|
sender: client.getUserId(),
|
||||||
|
stateKey: widgetId,
|
||||||
|
type: 'm.widget',
|
||||||
|
id: widgetId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setAccountData('m.widgets', userWidgets).then(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
dis.dispatch({ action: "user_widget_updated" });
|
||||||
|
});
|
||||||
|
} else { // Room widget
|
||||||
|
if (!roomId) {
|
||||||
|
sendError(event, _t('Missing roomId.'), null);
|
||||||
|
}
|
||||||
|
|
||||||
if (widgetUrl === null) { // widget is being deleted
|
if (widgetUrl === null) { // widget is being deleted
|
||||||
content = {};
|
content = {};
|
||||||
}
|
}
|
||||||
|
// TODO - Room widgets need to be moved to 'm.widget' state events
|
||||||
|
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
||||||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
||||||
sendResponse(event, {
|
sendResponse(event, {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -342,6 +380,7 @@ function setWidget(event, roomId) {
|
||||||
sendError(event, _t('Failed to send request.'), err);
|
sendError(event, _t('Failed to send request.'), err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getWidgets(event, roomId) {
|
function getWidgets(event, roomId) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -349,19 +388,30 @@ function getWidgets(event, roomId) {
|
||||||
sendError(event, _t('You need to be logged in.'));
|
sendError(event, _t('You need to be logged in.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let widgetStateEvents = [];
|
||||||
|
|
||||||
|
if (roomId) {
|
||||||
const room = client.getRoom(roomId);
|
const room = client.getRoom(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
sendError(event, _t('This room is not recognised.'));
|
sendError(event, _t('This room is not recognised.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// TODO - Room widgets need to be moved to 'm.widget' state events
|
||||||
|
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
||||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||||
// Only return widgets which have required fields
|
// Only return widgets which have required fields
|
||||||
const widgetStateEvents = [];
|
if (room) {
|
||||||
stateEvents.forEach((ev) => {
|
stateEvents.forEach((ev) => {
|
||||||
if (ev.getContent().type && ev.getContent().url) {
|
if (ev.getContent().type && ev.getContent().url) {
|
||||||
widgetStateEvents.push(ev.event); // return the raw event
|
widgetStateEvents.push(ev.event); // return the raw event
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user widgets (not linked to a specific room)
|
||||||
|
const userWidgets = Widgets.getUserWidgetsArray();
|
||||||
|
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
||||||
|
|
||||||
sendResponse(event, widgetStateEvents);
|
sendResponse(event, widgetStateEvents);
|
||||||
}
|
}
|
||||||
|
@ -578,10 +628,23 @@ const onMessage = function(event) {
|
||||||
|
|
||||||
const roomId = event.data.room_id;
|
const roomId = event.data.room_id;
|
||||||
const userId = event.data.user_id;
|
const userId = event.data.user_id;
|
||||||
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
|
// These APIs don't require roomId
|
||||||
|
// Get and set user widgets (not associated with a specific room)
|
||||||
|
// If roomId is specified, it must be validated, so room-based widgets agreed
|
||||||
|
// handled further down.
|
||||||
|
if (event.data.action === "get_widgets") {
|
||||||
|
getWidgets(event, null);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "set_widget") {
|
||||||
|
setWidget(event, null);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
sendError(event, _t('Missing room_id in request'));
|
sendError(event, _t('Missing room_id in request'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let promise = Promise.resolve(currentRoomId);
|
let promise = Promise.resolve(currentRoomId);
|
||||||
if (!currentRoomId) {
|
if (!currentRoomId) {
|
||||||
if (!currentRoomAlias) {
|
if (!currentRoomAlias) {
|
||||||
|
@ -601,6 +664,15 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get and set room-based widgets
|
||||||
|
if (event.data.action === "get_widgets") {
|
||||||
|
getWidgets(event, roomId);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "set_widget") {
|
||||||
|
setWidget(event, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// These APIs don't require userId
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
|
@ -611,12 +683,6 @@ const onMessage = function(event) {
|
||||||
} else if (event.data.action === "get_membership_count") {
|
} else if (event.data.action === "get_membership_count") {
|
||||||
getMembershipCount(event, roomId);
|
getMembershipCount(event, roomId);
|
||||||
return;
|
return;
|
||||||
} else if (event.data.action === "set_widget") {
|
|
||||||
setWidget(event, roomId);
|
|
||||||
return;
|
|
||||||
} else if (event.data.action === "get_widgets") {
|
|
||||||
getWidgets(event, roomId);
|
|
||||||
return;
|
|
||||||
} else if (event.data.action === "get_room_enc_state") {
|
} else if (event.data.action === "get_room_enc_state") {
|
||||||
getRoomEncState(event, roomId);
|
getRoomEncState(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
|
86
src/ToWidgetPostMessageApi.js
Normal file
86
src/ToWidgetPostMessageApi.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
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 Promise from "bluebird";
|
||||||
|
|
||||||
|
// const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
|
// Initiate requests using the "toWidget" postMessage API and handle responses
|
||||||
|
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
||||||
|
// response field
|
||||||
|
export default class ToWidgetPostMessageApi {
|
||||||
|
constructor(timeoutMs) {
|
||||||
|
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
||||||
|
this._counter = 0;
|
||||||
|
this._requestMap = {
|
||||||
|
// $ID: {resolve, reject}
|
||||||
|
};
|
||||||
|
this.start = this.start.bind(this);
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
this.onPostMessage = this.onPostMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
window.addEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
window.removeEventListener('message', this.onPostMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPostMessage(ev) {
|
||||||
|
// THIS IS ALL UNSAFE EXECUTION.
|
||||||
|
// We do not verify who the sender of `ev` is!
|
||||||
|
const payload = ev.data;
|
||||||
|
// NOTE: Workaround for running in a mobile WebView where a
|
||||||
|
// postMessage immediately triggers this callback even though it is
|
||||||
|
// not the response.
|
||||||
|
if (payload.response === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const promise = this._requestMap[payload._id];
|
||||||
|
if (!promise) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this._requestMap[payload._id];
|
||||||
|
promise.resolve(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate outbound requests (toWidget)
|
||||||
|
exec(action, targetWindow, targetOrigin) {
|
||||||
|
targetWindow = targetWindow || window.parent; // default to parent window
|
||||||
|
targetOrigin = targetOrigin || "*";
|
||||||
|
this._counter += 1;
|
||||||
|
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._requestMap[action._id] = {resolve, reject};
|
||||||
|
targetWindow.postMessage(action, targetOrigin);
|
||||||
|
|
||||||
|
if (this._timeoutMs > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this._requestMap[action._id]) {
|
||||||
|
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._timeoutMs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,312 +15,91 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
|
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
||||||
{
|
* spec. details / documentation.
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID,
|
|
||||||
data: {}
|
|
||||||
// additional request fields
|
|
||||||
}
|
|
||||||
|
|
||||||
The complete request object is returned to the caller with an additional "response" key like so:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID,
|
|
||||||
data: {},
|
|
||||||
// additional request fields
|
|
||||||
response: { ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
The "api" field is required to use this API, and must be set to "widget" in all requests.
|
|
||||||
|
|
||||||
The "action" determines the format of the request and response. All actions can return an error response.
|
|
||||||
|
|
||||||
Additional data can be sent as additional, abritrary fields. However, typically the data object should be used.
|
|
||||||
|
|
||||||
A success response is an object with zero or more keys.
|
|
||||||
|
|
||||||
An error response is a "response" object which consists of a sole "error" key to indicate an error.
|
|
||||||
They look like:
|
|
||||||
{
|
|
||||||
error: {
|
|
||||||
message: "Unable to invite user into room.",
|
|
||||||
_error: <Original Error Object>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
The "message" key should be a human-friendly string.
|
|
||||||
|
|
||||||
ACTIONS
|
|
||||||
=======
|
|
||||||
** All actions must include an "api" field with valie "widget".**
|
|
||||||
All actions can return an error response instead of the response outlined below.
|
|
||||||
|
|
||||||
content_loaded
|
|
||||||
--------------
|
|
||||||
Indicates that widget contet has fully loaded
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- widgetId is the unique ID of the widget instance in riot / matrix state.
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
success: true
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "content_loaded",
|
|
||||||
widgetId: $WIDGET_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
api_version
|
|
||||||
-----------
|
|
||||||
Get the current version of the widget postMessage API
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
api_version: "0.0.1"
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "api_version",
|
|
||||||
}
|
|
||||||
|
|
||||||
supported_api_versions
|
|
||||||
----------------------
|
|
||||||
Get versions of the widget postMessage API that are currently supported
|
|
||||||
|
|
||||||
Request:
|
|
||||||
- No additional fields.
|
|
||||||
Response:
|
|
||||||
{
|
|
||||||
api: "widget"
|
|
||||||
supported_versions: ["0.0.1"]
|
|
||||||
}
|
|
||||||
Example:
|
|
||||||
{
|
|
||||||
api: "widget",
|
|
||||||
action: "supported_api_versions",
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import URL from 'url';
|
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
||||||
|
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
if (!global.mxFromWidgetMessaging) {
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
||||||
'0.0.1',
|
global.mxFromWidgetMessaging.start();
|
||||||
];
|
|
||||||
|
|
||||||
import dis from './dispatcher';
|
|
||||||
|
|
||||||
if (!global.mxWidgetMessagingListenerCount) {
|
|
||||||
global.mxWidgetMessagingListenerCount = 0;
|
|
||||||
}
|
}
|
||||||
if (!global.mxWidgetMessagingMessageEndpoints) {
|
if (!global.mxToWidgetMessaging) {
|
||||||
global.mxWidgetMessagingMessageEndpoints = [];
|
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
||||||
|
global.mxToWidgetMessaging.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OUTBOUND_API_NAME = 'toWidget';
|
||||||
|
|
||||||
/**
|
export default class WidgetMessaging {
|
||||||
* Register widget message event listeners
|
constructor(widgetId, widgetUrl, target) {
|
||||||
*/
|
|
||||||
function startListening() {
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.addEventListener("message", onMessage, false);
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingListenerCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register widget message event listeners
|
|
||||||
*/
|
|
||||||
function stopListening() {
|
|
||||||
global.mxWidgetMessagingListenerCount -= 1;
|
|
||||||
if (global.mxWidgetMessagingListenerCount === 0) {
|
|
||||||
window.removeEventListener("message", onMessage);
|
|
||||||
}
|
|
||||||
if (global.mxWidgetMessagingListenerCount < 0) {
|
|
||||||
// Make an error so we get a stack trace
|
|
||||||
const e = new Error(
|
|
||||||
"WidgetMessaging: mismatched startListening / stopListening detected." +
|
|
||||||
" Negative count",
|
|
||||||
);
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a widget endpoint for trusted postMessage communication
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
*/
|
|
||||||
function addEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin:", endpointUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
const endpoint = new WidgetMessageEndpoint(widgetId, origin);
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints) {
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints.some(function(ep) {
|
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
|
||||||
})) {
|
|
||||||
// Message endpoint already registered
|
|
||||||
console.warn("Endpoint already registered");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
global.mxWidgetMessagingMessageEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register a widget endpoint from trusted communication sources
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
|
||||||
*/
|
|
||||||
function removeEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn("Invalid origin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
if (global.mxWidgetMessagingMessageEndpoints && global.mxWidgetMessagingMessageEndpoints.length > 0) {
|
|
||||||
const length = global.mxWidgetMessagingMessageEndpoints.length;
|
|
||||||
global.mxWidgetMessagingMessageEndpoints = global.mxWidgetMessagingMessageEndpoints.filter(function(endpoint) {
|
|
||||||
return (endpoint.widgetId != widgetId || endpoint.endpointUrl != origin);
|
|
||||||
});
|
|
||||||
return (length > global.mxWidgetMessagingMessageEndpoints.length);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle widget postMessage events
|
|
||||||
* @param {Event} event Event to handle
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
function onMessage(event) {
|
|
||||||
if (!event.origin) { // Handle chrome
|
|
||||||
event.origin = event.originalEvent.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== "widget" ||
|
|
||||||
!event.data.widgetId
|
|
||||||
) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = event.data.action;
|
|
||||||
const widgetId = event.data.widgetId;
|
|
||||||
if (action === 'content_loaded') {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'widget_content_loaded',
|
|
||||||
widgetId: widgetId,
|
|
||||||
});
|
|
||||||
sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
sendResponse(event, {
|
|
||||||
api: "widget",
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("Widget postMessage event unhandled");
|
|
||||||
sendError(event, {message: "The postMessage was unhandled"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if message origin is registered as trusted
|
|
||||||
* @param {string} origin PostMessage origin to check
|
|
||||||
* @return {boolean} True if trusted
|
|
||||||
*/
|
|
||||||
function trustedEndpoint(origin) {
|
|
||||||
if (!origin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return global.mxWidgetMessagingMessageEndpoints.some((endpoint) => {
|
|
||||||
return endpoint.endpointUrl === origin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a postmessage response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {Object} res Response data
|
|
||||||
*/
|
|
||||||
function sendResponse(event, res) {
|
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
|
||||||
data.response = res;
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an error response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {string} msg Error message
|
|
||||||
* @param {Error} nestedError Nested error event (optional)
|
|
||||||
*/
|
|
||||||
function sendError(event, msg, nestedError) {
|
|
||||||
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
|
||||||
data.response = {
|
|
||||||
error: {
|
|
||||||
message: msg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (nestedError) {
|
|
||||||
data.response.error._error = nestedError;
|
|
||||||
}
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
|
||||||
*/
|
|
||||||
class WidgetMessageEndpoint {
|
|
||||||
/**
|
|
||||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin.
|
|
||||||
*/
|
|
||||||
constructor(widgetId, endpointUrl) {
|
|
||||||
if (!widgetId) {
|
|
||||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
if (!endpointUrl) {
|
|
||||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
this.widgetId = widgetId;
|
this.widgetId = widgetId;
|
||||||
this.endpointUrl = endpointUrl;
|
this.widgetUrl = widgetUrl;
|
||||||
}
|
this.target = target;
|
||||||
|
this.fromWidget = global.mxFromWidgetMessaging;
|
||||||
|
this.toWidget = global.mxToWidgetMessaging;
|
||||||
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
messageToWidget(action) {
|
||||||
startListening: startListening,
|
return this.toWidget.exec(action, this.target).then((data) => {
|
||||||
stopListening: stopListening,
|
// Check for errors and reject if found
|
||||||
addEndpoint: addEndpoint,
|
if (data.response === undefined) { // null is valid
|
||||||
removeEndpoint: removeEndpoint,
|
throw new Error("Missing 'response' field");
|
||||||
};
|
}
|
||||||
|
if (data.response && data.response.error) {
|
||||||
|
const err = data.response.error;
|
||||||
|
const msg = String(err.message ? err.message : "An error was returned");
|
||||||
|
if (err._error) {
|
||||||
|
console.error(err._error);
|
||||||
|
}
|
||||||
|
// Potential XSS attack if 'msg' is not appropriately sanitized,
|
||||||
|
// as it is untrusted input by our parent window (which we assume is Riot).
|
||||||
|
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
// Return the response field for the request
|
||||||
|
return data.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a screenshot from a widget
|
||||||
|
* @return {Promise} To be resolved with screenshot data when it has been generated
|
||||||
|
*/
|
||||||
|
getScreenshot() {
|
||||||
|
console.warn('Requesting screenshot for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
|
api: OUTBOUND_API_NAME,
|
||||||
|
action: "screenshot",
|
||||||
|
})
|
||||||
|
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
||||||
|
.then((response) => response.screenshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request capabilities required by the widget
|
||||||
|
* @return {Promise} To be resolved with an array of requested widget capabilities
|
||||||
|
*/
|
||||||
|
getCapabilities() {
|
||||||
|
console.warn('Requesting capabilities for', this.widgetId);
|
||||||
|
return this.messageToWidget({
|
||||||
|
api: OUTBOUND_API_NAME,
|
||||||
|
action: "capabilities",
|
||||||
|
}).then((response) => {
|
||||||
|
console.warn('Got capabilities for', this.widgetId, response.capabilities);
|
||||||
|
return response.capabilities;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
37
src/WidgetMessagingEndpoint.js
Normal file
37
src/WidgetMessagingEndpoint.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
||||||
|
*/
|
||||||
|
export default class WidgetMessageEndpoint {
|
||||||
|
/**
|
||||||
|
* Mapping of widget instance to URL for trusted postMessage communication.
|
||||||
|
* @param {string} widgetId Unique widget identifier
|
||||||
|
* @param {string} endpointUrl Widget wurl origin.
|
||||||
|
*/
|
||||||
|
constructor(widgetId, endpointUrl) {
|
||||||
|
if (!widgetId) {
|
||||||
|
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
if (!endpointUrl) {
|
||||||
|
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
||||||
|
}
|
||||||
|
this.widgetId = widgetId;
|
||||||
|
this.endpointUrl = endpointUrl;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
import MatrixClientPeg from './MatrixClientPeg';
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
|
||||||
export default class WidgetUtils {
|
export default class WidgetUtils {
|
||||||
|
|
||||||
/* Returns true if user is able to send state events to modify widgets in this room
|
/* Returns true if user is able to send state events to modify widgets in this room
|
||||||
|
* (Does not apply to non-room-based / user widgets)
|
||||||
* @param roomId -- The ID of the room to check
|
* @param roomId -- The ID of the room to check
|
||||||
* @return Boolean -- true if the user can modify widgets in this room
|
* @return Boolean -- true if the user can modify widgets in this room
|
||||||
* @throws Error -- specifies the error reason
|
* @throws Error -- specifies the error reason
|
||||||
|
|
|
@ -52,14 +52,19 @@ module.exports = {
|
||||||
createMenu: function(Element, props) {
|
createMenu: function(Element, props) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
const closeMenu = function() {
|
const closeMenu = function(...args) {
|
||||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||||
|
|
||||||
if (props && props.onFinished) {
|
if (props && props.onFinished) {
|
||||||
props.onFinished.apply(null, arguments);
|
props.onFinished.apply(null, args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Close the menu on window resize
|
||||||
|
const windowResize = function() {
|
||||||
|
closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
const position = {};
|
const position = {};
|
||||||
let chevronFace = null;
|
let chevronFace = null;
|
||||||
|
|
||||||
|
@ -130,13 +135,17 @@ module.exports = {
|
||||||
menuStyle["backgroundColor"] = props.menuColour;
|
menuStyle["backgroundColor"] = props.menuColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isNaN(Number(props.menuPaddingTop))) {
|
||||||
|
menuStyle["paddingTop"] = props.menuPaddingTop;
|
||||||
|
}
|
||||||
|
|
||||||
// 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 = (
|
const menu = (
|
||||||
<div className={className} style={position}>
|
<div className={className} style={position}>
|
||||||
<div className={menuClasses} style={menuStyle}>
|
<div className={menuClasses} style={menuStyle}>
|
||||||
{ chevron }
|
{ chevron }
|
||||||
<Element {...props} onFinished={closeMenu} />
|
<Element {...props} onFinished={closeMenu} onResize={windowResize} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||||
<style>{ chevronCSS }</style>
|
<style>{ chevronCSS }</style>
|
||||||
|
|
|
@ -68,6 +68,9 @@ const FilePanel = React.createClass({
|
||||||
"room": {
|
"room": {
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"contains_url": true,
|
"contains_url": true,
|
||||||
|
"not_types": [
|
||||||
|
"m.sticker",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -450,7 +450,12 @@ module.exports = React.createClass({
|
||||||
if (prevEvent !== null
|
if (prevEvent !== null
|
||||||
&& prevEvent.sender && mxEv.sender
|
&& prevEvent.sender && mxEv.sender
|
||||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||||
&& mxEv.getType() == prevEvent.getType()) {
|
// The preferred way of checking for 'continuation messages' is by
|
||||||
|
// checking whether subsiquent messages from the same user have a
|
||||||
|
// message body. This is because all messages intended to be displayed
|
||||||
|
// should have a 'body' whereas some (non-m.room) messages (such as
|
||||||
|
// m.sticker) may not have a message 'type'.
|
||||||
|
&& Boolean(mxEv.getContent().body) == Boolean(prevEvent.getContent().body)) {
|
||||||
continuation = true;
|
continuation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -467,6 +467,15 @@ module.exports = React.createClass({
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this._checkIfAlone(this.state.room);
|
this._checkIfAlone(this.state.room);
|
||||||
break;
|
break;
|
||||||
|
case 'post_sticker_message':
|
||||||
|
this.injectSticker(
|
||||||
|
payload.data.content.url,
|
||||||
|
payload.data.content.info,
|
||||||
|
payload.data.description || payload.data.name);
|
||||||
|
break;
|
||||||
|
case 'picture_snapshot':
|
||||||
|
this.uploadFile(payload.file);
|
||||||
|
break;
|
||||||
case 'notifier_enabled':
|
case 'notifier_enabled':
|
||||||
case 'upload_failed':
|
case 'upload_failed':
|
||||||
case 'upload_started':
|
case 'upload_started':
|
||||||
|
@ -907,7 +916,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
ContentMessages.sendContentToRoom(
|
ContentMessages.sendContentToRoom(
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
file, this.state.room.roomId, MatrixClientPeg.get(),
|
||||||
).done(undefined, (error) => {
|
).catch((error) => {
|
||||||
if (error.name === "UnknownDeviceError") {
|
if (error.name === "UnknownDeviceError") {
|
||||||
// Let the staus bar handle this
|
// Let the staus bar handle this
|
||||||
return;
|
return;
|
||||||
|
@ -916,11 +925,27 @@ module.exports = React.createClass({
|
||||||
console.error("Failed to upload file " + file + " " + error);
|
console.error("Failed to upload file " + file + " " + error);
|
||||||
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, {
|
||||||
title: _t('Failed to upload file'),
|
title: _t('Failed to upload file'),
|
||||||
description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
description: ((error && error.message)
|
||||||
|
? error.message : _t("Server may be unavailable, overloaded, or the file too big")),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
injectSticker: function(url, info, text) {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
dis.dispatch({action: 'view_set_mxid'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentMessages.sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get())
|
||||||
|
.done(undefined, (error) => {
|
||||||
|
if (error.name === "UnknownDeviceError") {
|
||||||
|
// Let the staus bar handle this
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onSearch: function(term, scope) {
|
onSearch: function(term, scope) {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchTerm: term,
|
searchTerm: term,
|
||||||
|
@ -1603,7 +1628,8 @@ module.exports = React.createClass({
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
showApps={this.state.showApps}
|
||||||
|
hideAppsDrawer={this.state.editingRoomSettings} >
|
||||||
{ aux }
|
{ aux }
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/**
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -36,32 +36,23 @@ import WidgetUtils from '../../../WidgetUtils';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||||
|
const ENABLE_REACT_PERF = false;
|
||||||
|
|
||||||
export default React.createClass({
|
export default class AppTile extends React.Component {
|
||||||
displayName: 'AppTile',
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = this._getNewState(props);
|
||||||
|
|
||||||
propTypes: {
|
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||||
id: PropTypes.string.isRequired,
|
this._onMessage = this._onMessage.bind(this);
|
||||||
url: PropTypes.string.isRequired,
|
this._onLoaded = this._onLoaded.bind(this);
|
||||||
name: PropTypes.string.isRequired,
|
this._onEditClick = this._onEditClick.bind(this);
|
||||||
room: PropTypes.object.isRequired,
|
this._onDeleteClick = this._onDeleteClick.bind(this);
|
||||||
type: PropTypes.string.isRequired,
|
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||||
fullWidth: PropTypes.bool,
|
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set initial component state when the App wUrl (widget URL) is being updated.
|
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||||
|
@ -83,8 +74,20 @@ export default React.createClass({
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
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
|
* Add widget instance specific parameters to pass in wUrl
|
||||||
|
@ -112,11 +115,7 @@ export default React.createClass({
|
||||||
u.query = params;
|
u.query = params;
|
||||||
|
|
||||||
return u.format();
|
return u.format();
|
||||||
},
|
}
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return this._getNewState(this.props);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
* 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;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
isMixedContent() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
|
@ -152,14 +151,36 @@ export default React.createClass({
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
WidgetMessaging.startListening();
|
|
||||||
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
|
||||||
window.addEventListener('message', this._onMessage, false);
|
|
||||||
this.setScalarToken();
|
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
|
* Adds a scalar token to the widget URL, if required
|
||||||
|
@ -211,13 +232,7 @@ export default React.createClass({
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
WidgetMessaging.stopListening();
|
|
||||||
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
|
||||||
window.removeEventListener('message', this._onMessage);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.url !== this.props.url) {
|
if (nextProps.url !== this.props.url) {
|
||||||
|
@ -232,8 +247,10 @@ export default React.createClass({
|
||||||
widgetPageTitle: nextProps.widgetPageTitle,
|
widgetPageTitle: nextProps.widgetPageTitle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// Legacy Jitsi widget messaging
|
||||||
|
// TODO -- This should be replaced with the new widget postMessaging API
|
||||||
_onMessage(event) {
|
_onMessage(event) {
|
||||||
if (this.props.type !== 'jitsi') {
|
if (this.props.type !== 'jitsi') {
|
||||||
return;
|
return;
|
||||||
|
@ -251,26 +268,47 @@ export default React.createClass({
|
||||||
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
|
||||||
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
PlatformPeg.get().setupScreenSharingForIframe(iframe);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_canUserModify() {
|
_canUserModify() {
|
||||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||||
},
|
}
|
||||||
|
|
||||||
_onEditClick(e) {
|
_onEditClick(e) {
|
||||||
console.log("Edit widget ID ", this.props.id);
|
console.log("Edit widget ID ", this.props.id);
|
||||||
|
if (this.props.onEditClick) {
|
||||||
|
this.props.onEditClick();
|
||||||
|
} else {
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
|
||||||
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
|
this.props.room, 'type_' + this.props.type, this.props.id);
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
src: src,
|
src: src,
|
||||||
}, "mx_IntegrationsManager");
|
}, "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,
|
/* If user has permission to modify widgets, delete the widget,
|
||||||
* otherwise revoke access for the widget to load in the user's browser
|
* otherwise revoke access for the widget to load in the user's browser
|
||||||
*/
|
*/
|
||||||
_onDeleteClick() {
|
_onDeleteClick() {
|
||||||
|
if (this.props.onDeleteClick) {
|
||||||
|
this.props.onDeleteClick();
|
||||||
|
} else {
|
||||||
if (this._canUserModify()) {
|
if (this._canUserModify()) {
|
||||||
// Show delete confirmation dialog
|
// Show delete confirmation dialog
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
@ -292,6 +330,7 @@ export default React.createClass({
|
||||||
this.props.id,
|
this.props.id,
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
console.error('Failed to delete widget', e);
|
console.error('Failed to delete widget', e);
|
||||||
|
}).finally(() => {
|
||||||
this.setState({deleting: false});
|
this.setState({deleting: false});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -300,14 +339,69 @@ export default React.createClass({
|
||||||
console.log("Revoke widget permissions - %s", this.props.id);
|
console.log("Revoke widget permissions - %s", this.props.id);
|
||||||
this._revokeWidgetPermission();
|
this._revokeWidgetPermission();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when widget iframe has finished loading
|
* Called when widget iframe has finished loading
|
||||||
*/
|
*/
|
||||||
_onLoaded() {
|
_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});
|
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
|
* Set remote content title on AppTile
|
||||||
|
@ -321,7 +415,7 @@ export default React.createClass({
|
||||||
}, (err) =>{
|
}, (err) =>{
|
||||||
console.error("Failed to get page title", err);
|
console.error("Failed to get page title", err);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
// Widget labels to render, depending upon user permissions
|
// 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
|
// 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('Delete widget');
|
||||||
}
|
}
|
||||||
return _td('Revoke widget access');
|
return _td('Revoke widget access');
|
||||||
},
|
}
|
||||||
|
|
||||||
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
/* TODO -- Store permission in account data so that it is persisted across multiple devices */
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission() {
|
||||||
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
console.warn('Granting permission to load widget - ', this.state.widgetUrl);
|
||||||
localStorage.setItem(this.state.widgetPermissionId, true);
|
localStorage.setItem(this.state.widgetPermissionId, true);
|
||||||
this.setState({hasPermissionToLoad: true});
|
this.setState({hasPermissionToLoad: true});
|
||||||
},
|
}
|
||||||
|
|
||||||
_revokeWidgetPermission() {
|
_revokeWidgetPermission() {
|
||||||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||||
localStorage.removeItem(this.state.widgetPermissionId);
|
localStorage.removeItem(this.state.widgetPermissionId);
|
||||||
this.setState({hasPermissionToLoad: false});
|
this.setState({hasPermissionToLoad: false});
|
||||||
},
|
}
|
||||||
|
|
||||||
formatAppTileName() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
|
@ -351,7 +445,7 @@ export default React.createClass({
|
||||||
appTileName = this.props.name.trim();
|
appTileName = this.props.name.trim();
|
||||||
}
|
}
|
||||||
return appTileName;
|
return appTileName;
|
||||||
},
|
}
|
||||||
|
|
||||||
onClickMenuBar(ev) {
|
onClickMenuBar(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -366,16 +460,42 @@ export default React.createClass({
|
||||||
action: 'appsDrawer',
|
action: 'appsDrawer',
|
||||||
show: !this.props.show,
|
show: !this.props.show,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_getSafeUrl() {
|
_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 = '';
|
let safeWidgetUrl = '';
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
}
|
}
|
||||||
return safeWidgetUrl;
|
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() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
@ -399,7 +519,7 @@ export default React.createClass({
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
const loadingElement = (
|
const loadingElement = (
|
||||||
<div className='mx_AppTileBody mx_AppLoading'>
|
<div>
|
||||||
<MessageSpinner msg='Loading...' />
|
<MessageSpinner msg='Loading...' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -414,7 +534,7 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
appTileBody = (
|
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 }
|
{ this.state.loading && loadingElement }
|
||||||
{ /*
|
{ /*
|
||||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
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';
|
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');
|
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
<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}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
<span className="mx_AppTileMenuBarTitle">
|
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||||
<TintableSvgButton
|
{ this.props.showMinimise && <TintableSvgButton
|
||||||
src={windowStateIcon}
|
src={windowStateIcon}
|
||||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
title={_t('Minimize apps')}
|
title={_t('Minimize apps')}
|
||||||
width="10"
|
width="10"
|
||||||
height="10"
|
height="10"
|
||||||
/>
|
onClick={this._onMinimiseClick}
|
||||||
<b>{ this.formatAppTileName() }</b>
|
/> }
|
||||||
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
<span> - { this.state.widgetPageTitle }</span>
|
|
||||||
) }
|
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<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 */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <TintableSvgButton
|
{ showEditButton && <TintableSvgButton
|
||||||
src="img/edit_green.svg"
|
src="img/edit_green.svg"
|
||||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
className={"mx_AppTileMenuBarWidget " +
|
||||||
|
(this.props.showDelete ? "mx_AppTileMenuBarWidgetPadding" : "")}
|
||||||
title={_t('Edit')}
|
title={_t('Edit')}
|
||||||
onClick={this._onEditClick}
|
onClick={this._onEditClick}
|
||||||
width="10"
|
width="10"
|
||||||
|
@ -486,18 +619,71 @@ export default React.createClass({
|
||||||
/> }
|
/> }
|
||||||
|
|
||||||
{ /* Delete widget */ }
|
{ /* Delete widget */ }
|
||||||
<TintableSvgButton
|
{ this.props.showDelete && <TintableSvgButton
|
||||||
src={deleteIcon}
|
src={deleteIcon}
|
||||||
className={deleteClasses}
|
className={deleteClasses}
|
||||||
title={_t(deleteWidgetLabel)}
|
title={_t(deleteWidgetLabel)}
|
||||||
onClick={this._onDeleteClick}
|
onClick={this._onDeleteClick}
|
||||||
width="10"
|
width="10"
|
||||||
height="10"
|
height="10"
|
||||||
/>
|
/> }
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> }
|
||||||
{ appTileBody }
|
{ appTileBody }
|
||||||
</div>
|
</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");
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
Modal.createDialog(IntegrationsManager, {
|
Modal.createDialog(IntegrationsManager, {
|
||||||
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.roomId) :
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) :
|
||||||
null,
|
null,
|
||||||
}, "mx_IntegrationsManager");
|
}, "mx_IntegrationsManager");
|
||||||
}
|
}
|
||||||
|
@ -103,5 +103,5 @@ export default class ManageIntegsButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
ManageIntegsButton.propTypes = {
|
ManageIntegsButton.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,35 +30,45 @@ import Promise from 'bluebird';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default class extends React.Component {
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody'
|
||||||
|
|
||||||
propTypes: {
|
static propTypes = {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
mxEvent: PropTypes.object.isRequired,
|
mxEvent: PropTypes.object.isRequired,
|
||||||
|
|
||||||
/* called when the image has loaded */
|
/* called when the image has loaded */
|
||||||
onWidgetLoad: PropTypes.func.isRequired,
|
onWidgetLoad: PropTypes.func.isRequired,
|
||||||
},
|
}
|
||||||
|
|
||||||
contextTypes: {
|
static contextTypes = {
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||||
},
|
}
|
||||||
|
|
||||||
getInitialState: function() {
|
constructor(props) {
|
||||||
return {
|
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,
|
decryptedUrl: null,
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
decryptedBlob: null,
|
decryptedBlob: null,
|
||||||
error: null,
|
error: null,
|
||||||
imgError: false,
|
imgError: false,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.unmounted = false;
|
this.unmounted = false;
|
||||||
this.context.matrixClient.on('sync', this.onClientSync);
|
this.context.matrixClient.on('sync', this.onClientSync);
|
||||||
},
|
}
|
||||||
|
|
||||||
onClientSync(syncState, prevState) {
|
onClientSync(syncState, prevState) {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
@ -71,9 +81,9 @@ module.exports = React.createClass({
|
||||||
imgError: false,
|
imgError: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
onClick: function onClick(ev) {
|
onClick(ev) {
|
||||||
if (ev.button == 0 && !ev.metaKey) {
|
if (ev.button == 0 && !ev.metaKey) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
@ -93,49 +103,49 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_isGif: function() {
|
_isGif() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
return (
|
return (
|
||||||
content &&
|
content &&
|
||||||
content.info &&
|
content.info &&
|
||||||
content.info.mimetype === "image/gif"
|
content.info.mimetype === "image/gif"
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
onImageEnter: function(e) {
|
onImageEnter(e) {
|
||||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const imgElement = e.target;
|
const imgElement = e.target;
|
||||||
imgElement.src = this._getContentUrl();
|
imgElement.src = this._getContentUrl();
|
||||||
},
|
}
|
||||||
|
|
||||||
onImageLeave: function(e) {
|
onImageLeave(e) {
|
||||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const imgElement = e.target;
|
const imgElement = e.target;
|
||||||
imgElement.src = this._getThumbUrl();
|
imgElement.src = this._getThumbUrl();
|
||||||
},
|
}
|
||||||
|
|
||||||
onImageError: function() {
|
onImageError() {
|
||||||
this.setState({
|
this.setState({
|
||||||
imgError: true,
|
imgError: true,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
_getContentUrl: function() {
|
_getContentUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
return this.state.decryptedUrl;
|
return this.state.decryptedUrl;
|
||||||
} else {
|
} else {
|
||||||
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
return this.context.matrixClient.mxcUrlToHttp(content.url);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
_getThumbUrl: function() {
|
_getThumbUrl() {
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
// Don't use the thumbnail for clients wishing to autoplay gifs.
|
||||||
|
@ -146,9 +156,9 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
return this.context.matrixClient.mxcUrlToHttp(content.url, 800, 600);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.fixupHeight();
|
this.fixupHeight();
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
@ -182,23 +192,35 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
}
|
}
|
||||||
},
|
this._afterComponentDidMount();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
// To be overridden by subclasses (e.g. MStickerBody) for further
|
||||||
|
// initialisation after componentDidMount
|
||||||
|
_afterComponentDidMount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
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") {
|
if (payload.action === "timeline_resize") {
|
||||||
this.fixupHeight();
|
this.fixupHeight();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
fixupHeight: function() {
|
fixupHeight() {
|
||||||
if (!this.refs.image) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,10 +236,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
this.refs.image.style.height = thumbHeight + "px";
|
this.refs.image.style.height = thumbHeight + "px";
|
||||||
// console.log("Image height now", thumbHeight);
|
// console.log("Image height now", thumbHeight);
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
_messageContent(contentUrl, thumbUrl, content) {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
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();
|
const content = this.props.mxEvent.getContent();
|
||||||
|
|
||||||
if (this.state.error !== null) {
|
if (this.state.error !== null) {
|
||||||
|
@ -265,19 +302,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
return (
|
return this._messageContent(contentUrl, thumbUrl, content);
|
||||||
<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>
|
|
||||||
);
|
|
||||||
} else if (content.body) {
|
} else if (content.body) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageBody">
|
<span className="mx_MImageBody">
|
||||||
|
@ -291,5 +316,5 @@ module.exports = React.createClass({
|
||||||
</span>
|
</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,12 +65,16 @@ module.exports = React.createClass({
|
||||||
let BodyType = UnknownBody;
|
let BodyType = UnknownBody;
|
||||||
if (msgtype && bodyTypes[msgtype]) {
|
if (msgtype && bodyTypes[msgtype]) {
|
||||||
BodyType = bodyTypes[msgtype];
|
BodyType = bodyTypes[msgtype];
|
||||||
|
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||||
|
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||||
} else if (content.url) {
|
} else if (content.url) {
|
||||||
// Fallback to MFileBody if there's a content URL
|
// Fallback to MFileBody if there's a content URL
|
||||||
BodyType = bodyTypes['m.file'];
|
BodyType = bodyTypes['m.file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
return <BodyType
|
||||||
|
ref="body" mxEvent={this.props.mxEvent}
|
||||||
|
highlights={this.props.highlights}
|
||||||
highlightLink={this.props.highlightLink}
|
highlightLink={this.props.highlightLink}
|
||||||
showUrlPreview={this.props.showUrlPreview}
|
showUrlPreview={this.props.showUrlPreview}
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
|
|
|
@ -37,7 +37,15 @@ module.exports = React.createClass({
|
||||||
displayName: 'AppsDrawer',
|
displayName: 'AppsDrawer',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
userId: PropTypes.string.isRequired,
|
||||||
room: PropTypes.object.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() {
|
getInitialState: function() {
|
||||||
|
@ -48,7 +56,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
ScalarMessaging.startListening();
|
ScalarMessaging.startListening();
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
@ -58,7 +66,7 @@ module.exports = React.createClass({
|
||||||
this.scalarClient.connect().then(() => {
|
this.scalarClient.connect().then(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.log("Failed to connect to integrations server");
|
console.log('Failed to connect to integrations server');
|
||||||
// TODO -- Handle Scalar errors
|
// TODO -- Handle Scalar errors
|
||||||
// this.setState({
|
// this.setState({
|
||||||
// scalar_error: err,
|
// scalar_error: err,
|
||||||
|
@ -72,7 +80,7 @@ module.exports = React.createClass({
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
ScalarMessaging.stopListening();
|
ScalarMessaging.stopListening();
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
},
|
},
|
||||||
|
@ -83,7 +91,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(action) {
|
onAction: function(action) {
|
||||||
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case 'appsDrawer':
|
case 'appsDrawer':
|
||||||
// When opening the app drawer when there aren't any apps,
|
// When opening the app drawer when there aren't any apps,
|
||||||
|
@ -111,7 +119,7 @@ module.exports = React.createClass({
|
||||||
* passed through encodeURIComponent.
|
* passed through encodeURIComponent.
|
||||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||||
* @param {Object} variables The key/value pairs to replace the template
|
* @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'.
|
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||||
*/
|
*/
|
||||||
encodeUri: function(pathTemplate, variables) {
|
encodeUri: function(pathTemplate, variables) {
|
||||||
|
@ -192,13 +200,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_launchManageIntegrations: function() {
|
_launchManageIntegrations: function() {
|
||||||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager');
|
||||||
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
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;
|
null;
|
||||||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
|
||||||
src: src,
|
src: src,
|
||||||
}, "mx_IntegrationsManager");
|
}, 'mx_IntegrationsManager');
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickAddWidget: function(e) {
|
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
|
// Display a warning dialog if the max number of widgets have already been added to the room
|
||||||
const apps = this._getApps();
|
const apps = this._getApps();
|
||||||
if (apps && apps.length >= MAX_WIDGETS) {
|
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.`;
|
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
|
||||||
console.error(errorMsg);
|
console.error(errorMsg);
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Cannot add any more widgets"),
|
title: _t('Cannot add any more widgets'),
|
||||||
description: _t("The maximum permitted number of widgets have already been added to this room."),
|
description: _t('The maximum permitted number of widgets have already been added to this room.'),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -243,11 +251,11 @@ module.exports = React.createClass({
|
||||||
) {
|
) {
|
||||||
addWidget = <div
|
addWidget = <div
|
||||||
onClick={this.onClickAddWidget}
|
onClick={this.onClickAddWidget}
|
||||||
role="button"
|
role='button'
|
||||||
tabIndex="0"
|
tabIndex='0'
|
||||||
className={this.state.apps.length<2 ?
|
className={this.state.apps.length<2 ?
|
||||||
"mx_AddWidget_button mx_AddWidget_button_full_width" :
|
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||||
"mx_AddWidget_button"
|
'mx_AddWidget_button'
|
||||||
}
|
}
|
||||||
title={_t('Add a widget')}>
|
title={_t('Add a widget')}>
|
||||||
[+] { _t('Add a widget') }
|
[+] { _t('Add a widget') }
|
||||||
|
@ -255,8 +263,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_AppsDrawer">
|
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||||
<div id="apps" className="mx_AppsContainer">
|
<div id='apps' className='mx_AppsContainer'>
|
||||||
{ apps }
|
{ apps }
|
||||||
</div>
|
</div>
|
||||||
{ this._canUserModify() && addWidget }
|
{ this._canUserModify() && addWidget }
|
||||||
|
|
|
@ -32,7 +32,8 @@ module.exports = React.createClass({
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
userId: PropTypes.string.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
|
// Conference Handler implementation
|
||||||
conferenceHandler: PropTypes.object,
|
conferenceHandler: PropTypes.object,
|
||||||
|
@ -52,6 +53,11 @@ module.exports = React.createClass({
|
||||||
onResize: PropTypes.func,
|
onResize: PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defaultProps: {
|
||||||
|
showApps: true,
|
||||||
|
hideAppsDrawer: false,
|
||||||
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
||||||
!ObjectUtils.shallowEqual(this.state, nextState));
|
!ObjectUtils.shallowEqual(this.state, nextState));
|
||||||
|
@ -134,6 +140,7 @@ module.exports = React.createClass({
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
maxHeight={this.props.maxHeight}
|
maxHeight={this.props.maxHeight}
|
||||||
showApps={this.props.showApps}
|
showApps={this.props.showApps}
|
||||||
|
hide={this.props.hideAppsDrawer}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -36,6 +36,7 @@ const ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
'm.room.message': 'messages.MessageEvent',
|
'm.room.message': 'messages.MessageEvent',
|
||||||
|
'm.sticker': 'messages.MessageEvent',
|
||||||
'm.call.invite': 'messages.TextualEvent',
|
'm.call.invite': 'messages.TextualEvent',
|
||||||
'm.call.answer': 'messages.TextualEvent',
|
'm.call.answer': 'messages.TextualEvent',
|
||||||
'm.call.hangup': 'messages.TextualEvent',
|
'm.call.hangup': 'messages.TextualEvent',
|
||||||
|
@ -470,7 +471,8 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
const eventType = this.props.mxEvent.getType();
|
const eventType = this.props.mxEvent.getType();
|
||||||
|
|
||||||
// Info messages are basically information about commands processed on a room
|
// 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));
|
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
|
||||||
// This shouldn't happen: the caller should check we support this type
|
// 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 dis from '../../../dispatcher';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
import Stickerpicker from './Stickerpicker';
|
||||||
|
|
||||||
export default class MessageComposer extends React.Component {
|
export default class MessageComposer extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
|
@ -32,8 +32,6 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onCallClick = this.onCallClick.bind(this);
|
this.onCallClick = this.onCallClick.bind(this);
|
||||||
this.onHangupClick = this.onHangupClick.bind(this);
|
this.onHangupClick = this.onHangupClick.bind(this);
|
||||||
this.onUploadClick = this.onUploadClick.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.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||||
this.uploadFiles = this.uploadFiles.bind(this);
|
this.uploadFiles = this.uploadFiles.bind(this);
|
||||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||||
|
@ -202,20 +200,6 @@ export default class MessageComposer extends React.Component {
|
||||||
// this._startCallApp(true);
|
// 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}) {
|
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||||
this.setState({
|
this.setState({
|
||||||
autocompleteQuery: content,
|
autocompleteQuery: content,
|
||||||
|
@ -281,7 +265,12 @@ export default class MessageComposer extends React.Component {
|
||||||
alt={e2eTitle} title={e2eTitle}
|
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') {
|
if (this.props.callState && this.props.callState !== 'ended') {
|
||||||
hangupButton =
|
hangupButton =
|
||||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||||
|
@ -298,19 +287,6 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>;
|
</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(
|
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||||
MatrixClientPeg.get().credentials.userId);
|
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(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
ref={(c) => this.messageComposerInput = c}
|
ref={(c) => this.messageComposerInput = c}
|
||||||
|
@ -364,12 +345,11 @@ export default class MessageComposer extends React.Component {
|
||||||
onContentChanged={this.onInputContentChanged}
|
onContentChanged={this.onInputContentChanged}
|
||||||
onInputStateChanged={this.onInputStateChanged} />,
|
onInputStateChanged={this.onInputStateChanged} />,
|
||||||
formattingButton,
|
formattingButton,
|
||||||
|
stickerpickerButton,
|
||||||
uploadButton,
|
uploadButton,
|
||||||
hangupButton,
|
hangupButton,
|
||||||
callButton,
|
callButton,
|
||||||
videoCallButton,
|
videoCallButton,
|
||||||
showAppsButton,
|
|
||||||
hideAppsButton,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
controls.push(
|
||||||
|
|
|
@ -392,7 +392,7 @@ module.exports = React.createClass({
|
||||||
let manageIntegsButton;
|
let manageIntegsButton;
|
||||||
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
|
||||||
manageIntegsButton = <ManageIntegsButton
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,6 +255,13 @@
|
||||||
"Cannot add any more widgets": "Cannot add any more widgets",
|
"Cannot add any more widgets": "Cannot add any more widgets",
|
||||||
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
|
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
|
||||||
"Add a widget": "Add a widget",
|
"Add a widget": "Add a widget",
|
||||||
|
"Stickerpack": "Stickerpack",
|
||||||
|
"Sticker Messages": "Sticker Messages",
|
||||||
|
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||||
|
"Click": "Click",
|
||||||
|
"here": "here",
|
||||||
|
"to add some!": "to add some!",
|
||||||
|
"Add a stickerpack": "Add a stickerpack",
|
||||||
"Drop File Here": "Drop File Here",
|
"Drop File Here": "Drop File Here",
|
||||||
"Drop file here to upload": "Drop file here to upload",
|
"Drop file here to upload": "Drop file here to upload",
|
||||||
" (unsupported)": " (unsupported)",
|
" (unsupported)": " (unsupported)",
|
||||||
|
@ -324,6 +331,8 @@
|
||||||
"Video call": "Video call",
|
"Video call": "Video call",
|
||||||
"Hide Apps": "Hide Apps",
|
"Hide Apps": "Hide Apps",
|
||||||
"Show Apps": "Show Apps",
|
"Show Apps": "Show Apps",
|
||||||
|
"Hide Stickers": "Hide Stickers",
|
||||||
|
"Show Stickers": "Show Stickers",
|
||||||
"Upload file": "Upload file",
|
"Upload file": "Upload file",
|
||||||
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
|
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
|
||||||
"Send an encrypted reply…": "Send an encrypted reply…",
|
"Send an encrypted reply…": "Send an encrypted reply…",
|
||||||
|
@ -578,6 +587,7 @@
|
||||||
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
|
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
|
||||||
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
|
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
|
||||||
"Allow": "Allow",
|
"Allow": "Allow",
|
||||||
|
"Manage sticker packs": "Manage sticker packs",
|
||||||
"Delete Widget": "Delete Widget",
|
"Delete Widget": "Delete Widget",
|
||||||
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
|
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
|
||||||
"Delete widget": "Delete widget",
|
"Delete widget": "Delete widget",
|
||||||
|
|
|
@ -94,6 +94,18 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_tag_panel": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Tag Panel"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
"feature_sticker_messages": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Sticker Messages"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.dontSuggestEmoji": {
|
"MessageComposerInput.dontSuggestEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Disable Emoji suggestions while typing'),
|
displayName: _td('Disable Emoji suggestions while typing'),
|
||||||
|
|
91
src/utils/widgets.js
Normal file
91
src/utils/widgets.js
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all widgets (user and room) for the current user
|
||||||
|
* @param {object} room The room to get widgets for
|
||||||
|
* @return {[object]} Array containing current / active room and user widget state events
|
||||||
|
*/
|
||||||
|
function getWidgets(room) {
|
||||||
|
const widgets = getRoomWidgets(room);
|
||||||
|
widgets.concat(getUserWidgetsArray());
|
||||||
|
return widgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room specific widgets
|
||||||
|
* @param {object} room The room to get widgets force
|
||||||
|
* @return {[object]} Array containing current / active room widgets
|
||||||
|
*/
|
||||||
|
function getRoomWidgets(room) {
|
||||||
|
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||||
|
if (!appsStateEvents) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return appsStateEvents.filter((ev) => {
|
||||||
|
return ev.getContent().type && ev.getContent().url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user specific widgets (not linked to a specific room)
|
||||||
|
* @return {object} Event content object containing current / active user widgets
|
||||||
|
*/
|
||||||
|
function getUserWidgets() {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('User not logged in');
|
||||||
|
}
|
||||||
|
const userWidgets = client.getAccountData('m.widgets');
|
||||||
|
let userWidgetContent = {};
|
||||||
|
if (userWidgets && userWidgets.getContent()) {
|
||||||
|
userWidgetContent = userWidgets.getContent();
|
||||||
|
}
|
||||||
|
return userWidgetContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user specific widgets (not linked to a specific room) as an array
|
||||||
|
* @return {[object]} Array containing current / active user widgets
|
||||||
|
*/
|
||||||
|
function getUserWidgetsArray() {
|
||||||
|
return Object.values(getUserWidgets());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||||
|
* @return {[object]} Array containing current / active stickerpicker widgets
|
||||||
|
*/
|
||||||
|
function getStickerpickerWidgets() {
|
||||||
|
const widgets = getUserWidgetsArray();
|
||||||
|
const stickerpickerWidgets = widgets.filter((widget) => widget.type='m.stickerpicker');
|
||||||
|
return stickerpickerWidgets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||||
|
* @return {Promise} Resolves on account data updated
|
||||||
|
*/
|
||||||
|
function removeStickerpickerWidgets() {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('User not logged in');
|
||||||
|
}
|
||||||
|
const userWidgets = client.getAccountData('m.widgets').getContent() || {};
|
||||||
|
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||||
|
if (widget.type === 'm.stickerpicker') {
|
||||||
|
delete userWidgets[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return client.setAccountData('m.widgets', userWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getWidgets,
|
||||||
|
getRoomWidgets,
|
||||||
|
getUserWidgets,
|
||||||
|
getUserWidgetsArray,
|
||||||
|
getStickerpickerWidgets,
|
||||||
|
removeStickerpickerWidgets,
|
||||||
|
};
|
Loading…
Reference in a new issue