Merge branch 'develop' into dialog-a11y
This commit is contained in:
commit
f9b0243c8e
18 changed files with 488 additions and 52 deletions
|
@ -1,3 +1,9 @@
|
||||||
|
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
||||||
|
|
||||||
|
* Bump js-sdk version to pull in fix for [setting room publicity in a group](https://github.com/matrix-org/matrix-js-sdk/commit/aa3201ebb0fff5af2fb733080aa65ed1f7213de6).
|
||||||
|
|
||||||
Changes in [0.11.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.2) (2017-11-28)
|
Changes in [0.11.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.2) (2017-11-28)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.1...v0.11.2)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.1...v0.11.2)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.11.2",
|
"version": "0.11.3",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.9.1",
|
"matrix-js-sdk": "0.9.2",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"querystring": "^0.2.0",
|
"querystring": "^0.2.0",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* a selection of key codes, as used in KeyboardEvent.keyCode */
|
/* a selection of key codes, as used in KeyboardEvent.keyCode */
|
||||||
module.exports = {
|
export const KeyCode = {
|
||||||
BACKSPACE: 8,
|
BACKSPACE: 8,
|
||||||
TAB: 9,
|
TAB: 9,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
|
@ -58,3 +59,12 @@ module.exports = {
|
||||||
KEY_Y: 89,
|
KEY_Y: 89,
|
||||||
KEY_Z: 90,
|
KEY_Z: 90,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isOnlyCtrlOrCmdKeyEvent(ev) {
|
||||||
|
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||||
|
if (isMac) {
|
||||||
|
return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||||
|
} else {
|
||||||
|
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||||
|
}
|
||||||
|
}
|
|
@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) {
|
||||||
* Logs the current session out and transitions to the logged-out state
|
* Logs the current session out and transitions to the logged-out state
|
||||||
*/
|
*/
|
||||||
export function logout() {
|
export function logout() {
|
||||||
|
if (!MatrixClientPeg.get()) return;
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// logout doesn't work for guest sessions
|
// logout doesn't work for guest sessions
|
||||||
// Also we sometimes want to re-log in a guest session
|
// Also we sometimes want to re-log in a guest session
|
||||||
|
|
326
src/WidgetMessaging.js
Normal file
326
src/WidgetMessaging.js
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Listens for incoming postMessage requests from embedded widgets. The following API is exposed:
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
|
||||||
|
const WIDGET_API_VERSION = '0.0.1'; // Current API version
|
||||||
|
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||||
|
'0.0.1',
|
||||||
|
];
|
||||||
|
|
||||||
|
import dis from './dispatcher';
|
||||||
|
|
||||||
|
if (!global.mxWidgetMessagingListenerCount) {
|
||||||
|
global.mxWidgetMessagingListenerCount = 0;
|
||||||
|
}
|
||||||
|
if (!global.mxWidgetMessagingMessageEndpoints) {
|
||||||
|
global.mxWidgetMessagingMessageEndpoints = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register widget message event listeners
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
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.endpointUrl = endpointUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startListening: startListening,
|
||||||
|
stopListening: stopListening,
|
||||||
|
addEndpoint: addEndpoint,
|
||||||
|
removeEndpoint: removeEndpoint,
|
||||||
|
};
|
|
@ -53,9 +53,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import KeyCode from '../../KeyCode';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import CallMediaHandler from '../../CallMediaHandler';
|
import CallMediaHandler from '../../CallMediaHandler';
|
||||||
|
@ -153,13 +153,7 @@ export default React.createClass({
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.UP:
|
case KeyCode.UP:
|
||||||
|
|
|
@ -41,7 +41,7 @@ const rate_limited_func = require('../../ratelimitedfunc');
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
const Rooms = require('../../Rooms');
|
const Rooms = require('../../Rooms');
|
||||||
|
|
||||||
import KeyCode from '../../KeyCode';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
|
@ -433,13 +433,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown: function(ev) {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.keyCode) {
|
switch (ev.keyCode) {
|
||||||
case KeyCode.KEY_D:
|
case KeyCode.KEY_D:
|
||||||
|
|
|
@ -18,7 +18,7 @@ const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
const KeyCode = require('../../KeyCode');
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
|
||||||
const DEBUG_SCROLL = false;
|
const DEBUG_SCROLL = false;
|
||||||
// var DEBUG_SCROLL = true;
|
// var DEBUG_SCROLL = true;
|
||||||
|
|
|
@ -22,6 +22,7 @@ import FilterStore from '../../stores/FilterStore';
|
||||||
import FlairStore from '../../stores/FlairStore';
|
import FlairStore from '../../stores/FlairStore';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
|
|
||||||
const TagTile = React.createClass({
|
const TagTile = React.createClass({
|
||||||
displayName: 'TagTile',
|
displayName: 'TagTile',
|
||||||
|
@ -43,13 +44,11 @@ const TagTile = React.createClass({
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_group',
|
|
||||||
group_id: this.props.groupProfile.groupId,
|
|
||||||
});
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'select_tag',
|
action: 'select_tag',
|
||||||
tag: this.props.groupProfile.groupId,
|
tag: this.props.groupProfile.groupId,
|
||||||
|
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||||
|
shiftKey: e.shiftKey,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -148,6 +147,10 @@ export default React.createClass({
|
||||||
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map(
|
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map(
|
||||||
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
||||||
));
|
));
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'all_tags',
|
||||||
|
tags: joinedGroupIds,
|
||||||
|
});
|
||||||
this.setState({joinedGroupProfiles});
|
this.setState({joinedGroupProfiles});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ const dis = require("../../dispatcher");
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
const UserActivity = require("../../UserActivity");
|
const UserActivity = require("../../UserActivity");
|
||||||
const KeyCode = require('../../KeyCode');
|
import { KeyCode } from '../../Keyboard';
|
||||||
|
|
||||||
const PAGINATE_SIZE = 20;
|
const PAGINATE_SIZE = 20;
|
||||||
const INITIAL_SIZE = 20;
|
const INITIAL_SIZE = 20;
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
|
||||||
import * as KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import React from 'react';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
// The amount of time to wait for further changes to the input username before
|
// The amount of time to wait for further changes to the input username before
|
||||||
|
|
|
@ -22,6 +22,7 @@ import React from 'react';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import PlatformPeg from '../../../PlatformPeg';
|
import PlatformPeg from '../../../PlatformPeg';
|
||||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import WidgetMessaging from '../../../WidgetMessaging';
|
||||||
import TintableSvgButton from './TintableSvgButton';
|
import TintableSvgButton from './TintableSvgButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -71,16 +72,45 @@ export default React.createClass({
|
||||||
return {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
loading: true, // True while the iframe content is loading
|
loading: true, // True while the iframe content is loading
|
||||||
widgetUrl: newProps.url,
|
widgetUrl: this._addWurlParams(newProps.url),
|
||||||
widgetPermissionId: widgetPermissionId,
|
widgetPermissionId: widgetPermissionId,
|
||||||
// Assume that widget has permission to load if we are the user who
|
// Assume that widget has permission to load if we are the user who
|
||||||
// added it to the room, or if explicitly granted by the user
|
// added it to the room, or if explicitly granted by the user
|
||||||
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
hasPermissionToLoad: hasPermissionToLoad === 'true' || newProps.userId === newProps.creatorUserId,
|
||||||
error: null,
|
error: null,
|
||||||
deleting: false,
|
deleting: false,
|
||||||
|
widgetPageTitle: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add widget instance specific parameters to pass in wUrl
|
||||||
|
* Properties passed to widget instance:
|
||||||
|
* - widgetId
|
||||||
|
* - origin / parent URL
|
||||||
|
* @param {string} urlString Url string to modify
|
||||||
|
* @return {string}
|
||||||
|
* Url string with parameters appended.
|
||||||
|
* If url can not be parsed, it is returned unmodified.
|
||||||
|
*/
|
||||||
|
_addWurlParams(urlString) {
|
||||||
|
const u = url.parse(urlString);
|
||||||
|
if (!u) {
|
||||||
|
console.error("_addWurlParams", "Invalid URL", urlString);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = qs.parse(u.query);
|
||||||
|
// Append widget ID to query parameters
|
||||||
|
params.widgetId = this.props.id;
|
||||||
|
// Append current / parent URL
|
||||||
|
params.parentUrl = window.location.href;
|
||||||
|
u.search = undefined;
|
||||||
|
u.query = params;
|
||||||
|
|
||||||
|
return u.format();
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return this._getNewState(this.props);
|
return this._getNewState(this.props);
|
||||||
},
|
},
|
||||||
|
@ -122,6 +152,8 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
WidgetMessaging.startListening();
|
||||||
|
WidgetMessaging.addEndpoint(this.props.id, this.props.url);
|
||||||
window.addEventListener('message', this._onMessage, false);
|
window.addEventListener('message', this._onMessage, false);
|
||||||
this.setScalarToken();
|
this.setScalarToken();
|
||||||
},
|
},
|
||||||
|
@ -137,7 +169,7 @@ export default React.createClass({
|
||||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
widgetUrl: this.props.url,
|
widgetUrl: this._addWurlParams(this.props.url),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -150,7 +182,7 @@ export default React.createClass({
|
||||||
this._scalarClient.getScalarToken().done((token) => {
|
this._scalarClient.getScalarToken().done((token) => {
|
||||||
// Append scalar_token as a query param if not already present
|
// Append scalar_token as a query param if not already present
|
||||||
this._scalarClient.scalarToken = token;
|
this._scalarClient.scalarToken = token;
|
||||||
const u = url.parse(this.props.url);
|
const u = url.parse(this._addWurlParams(this.props.url));
|
||||||
const params = qs.parse(u.query);
|
const params = qs.parse(u.query);
|
||||||
if (!params.scalar_token) {
|
if (!params.scalar_token) {
|
||||||
params.scalar_token = encodeURIComponent(token);
|
params.scalar_token = encodeURIComponent(token);
|
||||||
|
@ -174,6 +206,8 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
WidgetMessaging.stopListening();
|
||||||
|
WidgetMessaging.removeEndpoint(this.props.id, this.props.url);
|
||||||
window.removeEventListener('message', this._onMessage);
|
window.removeEventListener('message', this._onMessage);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -256,10 +290,23 @@ export default React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when widget iframe has finished loading
|
||||||
|
*/
|
||||||
_onLoaded() {
|
_onLoaded() {
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set remote content title on AppTile
|
||||||
|
* @param {string} title Title string to set on the AppTile
|
||||||
|
*/
|
||||||
|
_updateWidgetTitle(title) {
|
||||||
|
if (title) {
|
||||||
|
this.setState({widgetPageTitle: null});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 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
|
||||||
_deleteWidgetLabel() {
|
_deleteWidgetLabel() {
|
||||||
|
@ -305,6 +352,15 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getSafeUrl() {
|
||||||
|
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
||||||
|
let safeWidgetUrl = '';
|
||||||
|
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
||||||
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
|
}
|
||||||
|
return safeWidgetUrl;
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
let appTileBody;
|
||||||
|
|
||||||
|
@ -320,11 +376,6 @@ export default React.createClass({
|
||||||
// a link to it.
|
// a link to it.
|
||||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||||
"allow-same-origin allow-scripts allow-presentation";
|
"allow-same-origin allow-scripts allow-presentation";
|
||||||
const parsedWidgetUrl = url.parse(this.state.widgetUrl);
|
|
||||||
let safeWidgetUrl = '';
|
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
const loadingElement = (
|
const loadingElement = (
|
||||||
|
@ -347,7 +398,7 @@ export default React.createClass({
|
||||||
{ this.state.loading && loadingElement }
|
{ this.state.loading && loadingElement }
|
||||||
<iframe
|
<iframe
|
||||||
ref="appFrame"
|
ref="appFrame"
|
||||||
src={safeWidgetUrl}
|
src={this._getSafeUrl()}
|
||||||
allowFullScreen="true"
|
allowFullScreen="true"
|
||||||
sandbox={sandboxFlags}
|
sandbox={sandboxFlags}
|
||||||
onLoad={this._onLoaded}
|
onLoad={this._onLoaded}
|
||||||
|
@ -383,6 +434,9 @@ export default React.createClass({
|
||||||
<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}>
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
<b>{ this.formatAppTileName() }</b>
|
<b>{ this.formatAppTileName() }</b>
|
||||||
|
{ this.state.widgetPageTitle && (
|
||||||
|
<span> - { this.state.widgetPageTitle }</span>
|
||||||
|
) }
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Edit widget */ }
|
{ /* Edit widget */ }
|
||||||
{ showEditButton && <TintableSvgButton
|
{ showEditButton && <TintableSvgButton
|
||||||
|
|
|
@ -230,6 +230,7 @@ module.exports = React.createClass({
|
||||||
<a href={contentUrl} onClick={this.onClick}>
|
<a href={contentUrl} onClick={this.onClick}>
|
||||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||||
alt={content.body}
|
alt={content.body}
|
||||||
|
onLoad={this.props.onWidgetLoad}
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave} />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode } from '../../../Keyboard';
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
|
|
@ -28,7 +28,7 @@ import Promise from 'bluebird';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
import SlashCommands from '../../../SlashCommands';
|
import SlashCommands from '../../../SlashCommands';
|
||||||
import KeyCode from '../../../KeyCode';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
|
@ -105,13 +105,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
|
||||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||||
let ctrlCmdOnly;
|
|
||||||
if (isMac) {
|
|
||||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
|
||||||
} else {
|
|
||||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
|
||||||
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
|
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
|
||||||
|
|
|
@ -18,7 +18,10 @@ import dis from '../dispatcher';
|
||||||
import Analytics from '../Analytics';
|
import Analytics from '../Analytics';
|
||||||
|
|
||||||
const INITIAL_STATE = {
|
const INITIAL_STATE = {
|
||||||
tags: [],
|
allTags: [],
|
||||||
|
selectedTags: [],
|
||||||
|
// Last selected tag when shift was not being pressed
|
||||||
|
anchorTag: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,15 +42,62 @@ class FilterStore extends Store {
|
||||||
|
|
||||||
__onDispatch(payload) {
|
__onDispatch(payload) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'select_tag':
|
case 'all_tags' :
|
||||||
this._setState({
|
this._setState({
|
||||||
tags: [payload.tag],
|
allTags: payload.tags,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
case 'select_tag': {
|
||||||
|
let newTags = [];
|
||||||
|
// Shift-click semantics
|
||||||
|
if (payload.shiftKey) {
|
||||||
|
// Select range of tags
|
||||||
|
let start = this._state.allTags.indexOf(this._state.anchorTag);
|
||||||
|
let end = this._state.allTags.indexOf(payload.tag);
|
||||||
|
|
||||||
|
if (start === -1) {
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
if (start > end) {
|
||||||
|
const temp = start;
|
||||||
|
start = end;
|
||||||
|
end = temp;
|
||||||
|
}
|
||||||
|
newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
|
||||||
|
newTags = [...new Set(
|
||||||
|
this._state.allTags.slice(start, end + 1).concat(newTags),
|
||||||
|
)];
|
||||||
|
} else {
|
||||||
|
if (payload.ctrlOrCmdKey) {
|
||||||
|
// Toggle individual tag
|
||||||
|
if (this._state.selectedTags.includes(payload.tag)) {
|
||||||
|
newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
|
||||||
|
} else {
|
||||||
|
newTags = [...this._state.selectedTags, payload.tag];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Select individual tag
|
||||||
|
newTags = [payload.tag];
|
||||||
|
}
|
||||||
|
// Only set the anchor tag if the tag was previously unselected, otherwise
|
||||||
|
// the next range starts with an unselected tag.
|
||||||
|
if (!this._state.selectedTags.includes(payload.tag)) {
|
||||||
|
this._setState({
|
||||||
|
anchorTag: payload.tag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setState({
|
||||||
|
selectedTags: newTags,
|
||||||
|
});
|
||||||
|
|
||||||
Analytics.trackEvent('FilterStore', 'select_tag');
|
Analytics.trackEvent('FilterStore', 'select_tag');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'deselect_tags':
|
case 'deselect_tags':
|
||||||
this._setState({
|
this._setState({
|
||||||
tags: [],
|
selectedTags: [],
|
||||||
});
|
});
|
||||||
Analytics.trackEvent('FilterStore', 'deselect_tags');
|
Analytics.trackEvent('FilterStore', 'deselect_tags');
|
||||||
break;
|
break;
|
||||||
|
@ -55,7 +105,7 @@ class FilterStore extends Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedTags() {
|
getSelectedTags() {
|
||||||
return this._state.tags;
|
return this._state.selectedTags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue