diff --git a/package.json b/package.json index 5c81db2153..eb2cabf854 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", - "commonmark": "^0.27.0", + "commonmark": "^0.28.1", "counterpart": "^0.18.0", "draft-js": "^0.11.0-alpha", "draft-js-export-html": "^0.6.0", @@ -77,6 +77,8 @@ "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", + "react-dnd": "^2.1.4", + "react-dnd-html5-backend": "^2.1.2", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", diff --git a/src/CallHandler.js b/src/CallHandler.js index dd9d93709f..fd56d7f1b1 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -58,6 +59,7 @@ import sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; +import { showUnknownDeviceDialogForCalls } from './cryptodevices'; global.mxCalls = { //room_id: MatrixCall @@ -97,19 +99,54 @@ function pause(audioId) { } } +function _reAttemptCall(call) { + if (call.direction === 'outbound') { + dis.dispatch({ + action: 'place_call', + room_id: call.roomId, + type: call.type, + }); + } else { + call.answer(); + } +} + function _setCallListeners(call) { call.on("error", function(err) { console.error("Call error: %s", err); console.error(err.stack); - call.hangup(); - _setCallState(undefined, call.roomId, "ended"); - }); - call.on('send_event_error', function(err) { - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: MatrixClientPeg.get().getRoom(call.roomId), + if (err.code === 'unknown_devices') { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { + title: _t('Call Failed'), + description: _t( + "There are unknown devices in this room: "+ + "if you proceed without verifying them, it will be "+ + "possible for someone to eavesdrop on your call." + ), + button: _t('Review Devices'), + onFinished: function(confirmed) { + if (confirmed) { + const room = MatrixClientPeg.get().getRoom(call.roomId); + showUnknownDeviceDialogForCalls( + MatrixClientPeg.get(), + room, + () => { + _reAttemptCall(call); + }, + call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"), + call.direction === 'outbound' ? _t("Call") : _t("Answer"), + ); + } + }, + }); + } else { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + Modal.createTrackedDialog('Call Failed', '', ErrorDialog, { + title: _t('Call Failed'), + description: err.message, }); } }); @@ -179,7 +216,6 @@ function _setCallState(call, roomId, status) { function _onAction(payload) { function placeCall(newCall) { _setCallListeners(newCall); - _setCallState(newCall, newCall.roomId, "ringback"); if (payload.type === 'voice') { newCall.placeVoiceCall(); } else if (payload.type === 'video') { diff --git a/src/Keyboard.js b/src/Keyboard.js index 9c872e1c66..bf83a1a05f 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) { return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; } } + +export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + if (isMac) { + return ev.metaKey && !ev.altKey && !ev.ctrlKey; + } else { + return ev.ctrlKey && !ev.altKey && !ev.metaKey; + } +} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 946f22537d..efd5c20d5c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) { * Logs the current session out and transitions to the logged-out state */ export function logout() { + if (!MatrixClientPeg.get()) return; + if (MatrixClientPeg.get().isGuest()) { // logout doesn't work for guest sessions // Also we sometimes want to re-log in a guest session diff --git a/src/Markdown.js b/src/Markdown.js index e05f163ba5..aa1c7e45b1 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -55,25 +55,6 @@ function is_multi_line(node) { return par.firstChild != par.lastChild; } -import linkifyMatrix from './linkify-matrix'; -import * as linkify from 'linkifyjs'; -linkifyMatrix(linkify); - -// Thieved from draft-js-export-markdown -function escapeMarkdown(s) { - return s.replace(/[*_`]/g, '\\$&'); -} - -// Replace URLs, room aliases and user IDs with md-escaped URLs -function linkifyMarkdown(s) { - const links = linkify.find(s); - links.forEach((l) => { - // This may replace several instances of `l.value` at once, but that's OK - s = s.replace(l.value, escapeMarkdown(l.value)); - }); - return s; -} - /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -81,7 +62,7 @@ function linkifyMarkdown(s) { */ export default class Markdown { constructor(input) { - this.input = linkifyMarkdown(input); + this.input = input; const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a6012f5213..14dfa91fa4 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. +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. @@ -22,6 +23,7 @@ import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; +import MatrixActionCreators from './actions/MatrixActionCreators'; interface MatrixClientCreds { homeserverUrl: string, @@ -68,6 +70,8 @@ class MatrixClientPeg { unset() { this.matrixClient = null; + + MatrixActionCreators.stop(); } /** @@ -108,6 +112,9 @@ class MatrixClientPeg { // regardless of errors, start the client. If we did error out, we'll // just end up doing a full initial /sync. + // Connect the matrix client to the dispatcher + MatrixActionCreators.start(this.matrixClient); + console.log(`MatrixClientPeg: really starting MatrixClient`); this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); diff --git a/src/Modal.js b/src/Modal.js index 68d75d1ff1..c9f08772e7 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,7 @@ limitations under the License. const React = require('react'); const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; @@ -33,7 +34,7 @@ const AsyncWrapper = React.createClass({ /** A function which takes a 'callback' argument which it will call * with the real component once it loads. */ - loader: React.PropTypes.func.isRequired, + loader: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/Notifier.js b/src/Notifier.js index 75b698862c..e69bdf4461 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -135,6 +135,10 @@ const Notifier = { const plaf = PlatformPeg.get(); if (!plaf) return; + // Dev note: We don't set the "notificationsEnabled" setting to true here because it is a + // calculated value. It is determined based upon whether or not the master rule is enabled + // and other flags. Setting it here would cause a circular reference. + Analytics.trackEvent('Notifier', 'Set Enabled', enable); // make sure that we persist the current setting audio_enabled setting @@ -168,7 +172,7 @@ const Notifier = { }); // clear the notifications_hidden flag, so that if notifications are // disabled again in the future, we will show the banner again. - this.setToolbarHidden(false); + this.setToolbarHidden(true); } else { dis.dispatch({ action: "notifier_enabled", diff --git a/src/Resend.js b/src/Resend.js index 1fee5854ea..4eaee16d1b 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -44,13 +44,6 @@ module.exports = { // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 console.log('Resend got send failure: ' + err.name + '('+err+')'); - if (err.name === "UnknownDeviceError") { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: room, - }); - } dis.dispatch({ action: 'message_send_failed', @@ -60,9 +53,5 @@ module.exports = { }, removeFromQueue: function(event) { MatrixClientPeg.get().cancelPendingEvent(event); - dis.dispatch({ - action: 'message_send_cancelled', - event: event, - }); }, }; diff --git a/src/Rooms.js b/src/Rooms.js index 6cc2d867a6..ffa39141ff 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -43,7 +43,7 @@ export function getOnlyOtherMember(room, me) { return null; } -export function isConfCallRoom(room, me, conferenceHandler) { +function _isConfCallRoom(room, me, conferenceHandler) { if (!conferenceHandler) return false; if (me.membership != "join") { @@ -58,6 +58,26 @@ export function isConfCallRoom(room, me, conferenceHandler) { if (conferenceHandler.isConferenceUser(otherMember.userId)) { return true; } + + return false; +} + +// Cache whether a room is a conference call. Assumes that rooms will always +// either will or will not be a conference call room. +const isConfCallRoomCache = { + // $roomId: bool +}; + +export function isConfCallRoom(room, me, conferenceHandler) { + if (isConfCallRoomCache[room.roomId] !== undefined) { + return isConfCallRoomCache[room.roomId]; + } + + const result = _isConfCallRoom(room, me, conferenceHandler); + + isConfCallRoomCache[room.roomId] = result; + + return result; } export function looksLikeDirectMessageRoom(room, me) { diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index c9d056f88e..7bd8603264 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -15,6 +15,7 @@ limitations under the License. */ import Promise from 'bluebird'; +import SettingsStore from "./settings/SettingsStore"; const request = require('browser-request'); const SdkConfig = require('./SdkConfig'); @@ -76,10 +77,40 @@ class ScalarAuthClient { return defer.promise; } + getScalarPageTitle(url) { + const defer = Promise.defer(); + + let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup'; + scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); + scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); + request({ + method: 'GET', + uri: scalarPageLookupUrl, + json: true, + }, (err, response, body) => { + if (err) { + defer.reject(err); + } else if (response.statusCode / 100 !== 2) { + defer.reject({statusCode: response.statusCode}); + } else if (!body) { + defer.reject(new Error("Missing page title in response")); + } else { + let title = ""; + if (body.page_title_cache_item && body.page_title_cache_item.cached_title) { + title = body.page_title_cache_item.cached_title; + } + defer.resolve(title); + } + }); + + return defer.promise; + } + getScalarInterfaceUrlForRoom(roomId, screen, id) { let url = SdkConfig.get().integrations_ui_url; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); + url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); if (id) { url += '&integ_id=' + encodeURIComponent(id); } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 7698829647..3c164c6551 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -366,6 +366,22 @@ function getWidgets(event, roomId) { sendResponse(event, widgetStateEvents); } +function getRoomEncState(event, roomId) { + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + const room = client.getRoom(roomId); + if (!room) { + sendError(event, _t('This room is not recognised.')); + return; + } + const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); + + sendResponse(event, roomIsEncrypted); +} + function setPlumbingState(event, roomId, status) { if (typeof status !== 'string') { throw new Error('Plumbing state status should be a string'); @@ -541,8 +557,16 @@ const onMessage = function(event) { // // All strings start with the empty string, so for sanity return if the length // of the event origin is 0. + // + // TODO -- Scalar postMessage API should be namespaced with event.data.api field + // Fix following "if" statement to respond only to specific API messages. const url = SdkConfig.get().integrations_ui_url; - if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) { + if ( + event.origin.length === 0 || + !url.startsWith(event.origin) || + !event.data.action || + event.data.api // Ignore messages with specific API set + ) { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } @@ -593,6 +617,9 @@ const onMessage = function(event) { } else if (event.data.action === "get_widgets") { getWidgets(event, roomId); return; + } else if (event.data.action === "get_room_enc_state") { + getRoomEncState(event, roomId); + return; } else if (event.data.action === "can_send_event") { canSendEvent(event, roomId); return; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 344bac1ddb..d45e45e84c 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -96,6 +96,8 @@ const commands = { colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; + } else { + colorScheme.secondary_color = colorScheme.primary_color; } return success( SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), @@ -295,7 +297,7 @@ const commands = { // Define the power level of a user op: new Command("op", " []", function(roomId, args) { if (args) { - const matches = args.match(/^(\S+?)( +(\d+))?$/); + const matches = args.match(/^(\S+?)( +(-?\d+))?$/); let powerLevel = 50; // default power level for op if (matches) { const userId = matches[1]; diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js deleted file mode 100644 index e7d77b3b66..0000000000 --- a/src/UnknownDeviceErrorHandler.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 Vector Creations 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 dis from './dispatcher'; -import sdk from './index'; -import Modal from './Modal'; - -let isDialogOpen = false; - -const onAction = function(payload) { - if (payload.action === 'unknown_device_error' && !isDialogOpen) { - const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); - isDialogOpen = true; - Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { - devices: payload.err.devices, - room: payload.room, - onFinished: (r) => { - isDialogOpen = false; - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('UnknownDeviceDialog closed with '+r); - }, - }, 'mx_Dialog_unknownDevice'); - } -}; - -let ref = null; - -export function startListening() { - ref = dis.register(onAction); -} - -export function stopListening() { - if (ref) { - dis.unregister(ref); - ref = null; - } -} diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 9a674d4f09..af4e6dcb60 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -1,5 +1,6 @@ const React = require('react'); const ReactDom = require('react-dom'); +import PropTypes from 'prop-types'; const Velocity = require('velocity-vector'); /** @@ -14,16 +15,16 @@ module.exports = React.createClass({ propTypes: { // either a list of child nodes, or a single child. - children: React.PropTypes.any, + children: PropTypes.any, // optional transition information for changing existing children - transition: React.PropTypes.object, + transition: PropTypes.object, // a list of state objects to apply to each child node in turn - startStyles: React.PropTypes.array, + startStyles: PropTypes.array, // a list of transition options from the corresponding startStyle - enterTransitionOpts: React.PropTypes.array, + enterTransitionOpts: PropTypes.array, }, getDefaultProps: function() { diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js new file mode 100644 index 0000000000..0f23413b5f --- /dev/null +++ b/src/WidgetMessaging.js @@ -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: + } +} +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:", 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.endpointUrl = endpointUrl; + } +} + +export default { + startListening: startListening, + stopListening: stopListening, + addEndpoint: addEndpoint, + removeEndpoint: removeEndpoint, +}; diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js new file mode 100644 index 0000000000..006c2da5b8 --- /dev/null +++ b/src/actions/GroupActions.js @@ -0,0 +1,34 @@ +/* +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 { asyncAction } from './actionCreators'; + +const GroupActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +GroupActions.fetchJoinedGroups = function(matrixClient) { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); +}; + +export default GroupActions; diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js new file mode 100644 index 0000000000..33bdb53799 --- /dev/null +++ b/src/actions/MatrixActionCreators.js @@ -0,0 +1,108 @@ +/* +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 dis from '../dispatcher'; + +// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events +// become dispatches in the same place. +/** + * Create a MatrixActions.sync action that represents a MatrixClient `sync` event, + * each parameter mapping to a key-value in the action. + * + * @param {MatrixClient} matrixClient the matrix client + * @param {string} state the current sync state. + * @param {string} prevState the previous sync state. + * @returns {Object} an action of type MatrixActions.sync. + */ +function createSyncAction(matrixClient, state, prevState) { + return { + action: 'MatrixActions.sync', + state, + prevState, + matrixClient, + }; +} + +/** + * @typedef AccountDataAction + * @type {Object} + * @property {string} action 'MatrixActions.accountData'. + * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. + * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". + * @property {Object} event_content the content of the MatrixEvent. + */ + +/** + * Create a MatrixActions.accountData action that represents a MatrixClient `accountData` + * matrix event. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} accountDataEvent the account data event. + * @returns {AccountDataAction} an action of type MatrixActions.accountData. + */ +function createAccountDataAction(matrixClient, accountDataEvent) { + return { + action: 'MatrixActions.accountData', + event: accountDataEvent, + event_type: accountDataEvent.getType(), + event_content: accountDataEvent.getContent(), + }; +} + +/** + * This object is responsible for dispatching actions when certain events are emitted by + * the given MatrixClient. + */ +export default { + // A list of callbacks to call to unregister all listeners added + _matrixClientListenersStop: [], + + /** + * Start listening to certain events from the MatrixClient and dispatch actions when + * they are emitted. + * @param {MatrixClient} matrixClient the MatrixClient to listen to events from + */ + start(matrixClient) { + this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); + this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + }, + + /** + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. + */ + _addMatrixClientListener(matrixClient, eventName, actionCreator) { + const listener = (...args) => { + dis.dispatch(actionCreator(matrixClient, ...args)); + }; + matrixClient.on(eventName, listener); + this._matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); + }, + + /** + * Stop listening to events. + */ + stop() { + this._matrixClientListenersStop.forEach((stopListener) => stopListener()); + }, +}; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js new file mode 100644 index 0000000000..60946ea7f1 --- /dev/null +++ b/src/actions/TagOrderActions.js @@ -0,0 +1,47 @@ +/* +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 Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; + +const TagOrderActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * commit TagOrderStore.getOrderedTags() to account data and dispatch + * actions to indicate the status of the request. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.commitTagOrdering = function(matrixClient) { + return asyncAction('TagOrderActions.commitTagOrdering', () => { + // Only commit tags if the state is ready, i.e. not null + const tags = TagOrderStore.getOrderedTags(); + if (!tags) { + return; + } + + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags}); + }); +}; + +export default TagOrderActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js new file mode 100644 index 0000000000..bddfbc7c63 --- /dev/null +++ b/src/actions/actionCreators.js @@ -0,0 +1,41 @@ +/* +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. +*/ + +/** + * Create an action thunk that will dispatch actions indicating the current + * status of the Promise returned by fn. + * + * @param {string} id the id to give the dispatched actions. This is given a + * suffix determining whether it is pending, successful or + * a failure. + * @param {function} fn a function that returns a Promise. + * @returns {function} an action thunk - a function that uses its single + * argument as a dispatch function to dispatch the + * following actions: + * `${id}.pending` and either + * `${id}.success` or + * `${id}.failure`. + */ +export function asyncAction(id, fn) { + return (dispatch) => { + dispatch({action: id + '.pending'}); + fn().then((result) => { + dispatch({action: id + '.success', result}); + }).catch((err) => { + dispatch({action: id + '.failure', err}); + }); + }; +} diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index a8f588d39a..5db8b2365f 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ const React = require("react"); +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -23,8 +24,8 @@ module.exports = React.createClass({ displayName: 'EncryptedEventDialog', propTypes: { - event: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func.isRequired, + event: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 04274442c2..06fb0668d5 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as Matrix from 'matrix-js-sdk'; @@ -29,8 +30,8 @@ export default React.createClass({ displayName: 'ExportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index a01b6580f1..10744a8911 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -40,8 +41,8 @@ export default React.createClass({ displayName: 'ImportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index a27533f7c2..b09f4e963e 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; /* These were earlier stateless functional components but had to be converted @@ -42,10 +43,10 @@ export class TextualCompletion extends React.Component { } } TextualCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + className: PropTypes.string, }; export class PillCompletion extends React.Component { @@ -69,9 +70,9 @@ export class PillCompletion extends React.Component { } } PillCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - initialComponent: React.PropTypes.element, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + initialComponent: PropTypes.element, + className: PropTypes.string, }; diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 5814f40437..fefe77f6cd 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -54,8 +54,10 @@ export default class UserProvider extends AutocompleteProvider { } destroy() { - MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); - MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound); + MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound); + } } _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 3c2308e6a7..94f5713a79 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -20,6 +20,7 @@ limitations under the License. const classNames = require('classnames'); const React = require('react'); const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -29,11 +30,11 @@ module.exports = { ContextualMenuContainerId: "mx_ContextualMenu_Container", propTypes: { - menuWidth: React.PropTypes.number, - menuHeight: React.PropTypes.number, - chevronOffset: React.PropTypes.number, - menuColour: React.PropTypes.string, - chevronFace: React.PropTypes.string, // top, bottom, left, right + menuWidth: PropTypes.number, + menuHeight: PropTypes.number, + chevronOffset: PropTypes.number, + menuColour: PropTypes.string, + chevronFace: PropTypes.string, // top, bottom, left, right }, getOrCreateContainer: function() { diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 26454c5ea6..2bb9adb544 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; @@ -30,8 +31,8 @@ module.exports = React.createClass({ displayName: 'CreateRoom', propTypes: { - onRoomCreated: React.PropTypes.func, - collapsedRhs: React.PropTypes.bool, + onRoomCreated: PropTypes.func, + collapsedRhs: PropTypes.bool, }, phases: { diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index ffa5e45249..e86b76333d 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; import sdk from '../../index'; @@ -28,7 +29,7 @@ const FilePanel = React.createClass({ displayName: 'FilePanel', propTypes: { - roomId: React.PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, }, getInitialState: function() { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 64f9955aa9..de96935838 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -391,7 +391,7 @@ const FeaturedUser = React.createClass({ }); const GroupContext = { - groupStore: React.PropTypes.instanceOf(GroupStore).isRequired, + groupStore: PropTypes.instanceOf(GroupStore).isRequired, }; CategoryRoomList.contextTypes = GroupContext; @@ -409,7 +409,7 @@ export default React.createClass({ }, childContextTypes: { - groupStore: React.PropTypes.instanceOf(GroupStore), + groupStore: PropTypes.instanceOf(GroupStore), }, getChildContext: function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 8a2c1b8c79..8428e3c714 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk'; const InteractiveAuth = Matrix.InteractiveAuth; import React from 'react'; +import PropTypes from 'prop-types'; import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; @@ -26,18 +27,18 @@ export default React.createClass({ propTypes: { // matrix client to use for UI auth requests - matrixClient: React.PropTypes.object.isRequired, + matrixClient: PropTypes.object.isRequired, // response from initial request. If not supplied, will do a request on // mount. - authData: React.PropTypes.shape({ - flows: React.PropTypes.array, - params: React.PropTypes.object, - session: React.PropTypes.string, + authData: PropTypes.shape({ + flows: PropTypes.array, + params: PropTypes.object, + session: PropTypes.string, }), // callback - makeRequest: React.PropTypes.func.isRequired, + makeRequest: PropTypes.func.isRequired, // callback called when the auth process has finished, // successfully or unsuccessfully. @@ -51,22 +52,22 @@ export default React.createClass({ // the auth session. // * clientSecret {string} The client secret used in auth // sessions with the ID server. - onAuthFinished: React.PropTypes.func.isRequired, + onAuthFinished: PropTypes.func.isRequired, // Inputs provided by the user to the auth process // and used by various stages. As passed to js-sdk // interactive-auth - inputs: React.PropTypes.object, + inputs: PropTypes.object, // As js-sdk interactive-auth - makeRegistrationUrl: React.PropTypes.func, - sessionId: React.PropTypes.string, - clientSecret: React.PropTypes.string, - emailSid: React.PropTypes.string, + makeRegistrationUrl: PropTypes.func, + sessionId: PropTypes.string, + clientSecret: PropTypes.string, + emailSid: PropTypes.string, // If true, poll to see if the auth flow has been completed // out-of-band - poll: React.PropTypes.bool, + poll: PropTypes.bool, }, getInitialState: function() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 01abf966f9..bebc109806 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -18,6 +18,9 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; +import PropTypes from 'prop-types'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; @@ -38,27 +41,27 @@ import SettingsStore from "../../settings/SettingsStore"; * * Components mounted below us can access the matrix client via the react context. */ -export default React.createClass({ +const LoggedInView = React.createClass({ displayName: 'LoggedInView', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - page_type: React.PropTypes.string.isRequired, - onRoomCreated: React.PropTypes.func, - onUserSettingsClose: React.PropTypes.func, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + page_type: PropTypes.string.isRequired, + onRoomCreated: PropTypes.func, + onUserSettingsClose: PropTypes.func, // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) - onRegistered: React.PropTypes.func, + onRegistered: PropTypes.func, - teamToken: React.PropTypes.string, + teamToken: PropTypes.string, // and lots and lots of other stuff. }, childContextTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient), - authCache: React.PropTypes.object, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient), + authCache: PropTypes.object, }, getChildContext: function() { @@ -331,7 +334,6 @@ export default React.createClass({
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? :
} @@ -344,3 +346,5 @@ export default React.createClass({ ); }, }); + +export default DragDropContext(HTML5Backend)(LoggedInView); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index cd75ad8798..733007677b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -19,6 +19,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import PropTypes from 'prop-types'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; @@ -40,7 +41,6 @@ require('../../stores/LifecycleStore'); import PageTypes from '../../PageTypes'; import createRoom from "../../createRoom"; -import * as UDEHandler from '../../UnknownDeviceErrorHandler'; import KeyRequestHandler from '../../KeyRequestHandler'; import { _t, getCurrentLanguage } from '../../languageHandler'; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; @@ -84,7 +84,7 @@ const ONBOARDING_FLOW_STARTERS = [ 'view_create_group', ]; -module.exports = React.createClass({ +export default React.createClass({ // we export this so that the integration tests can use it :-S statics: { VIEWS: VIEWS, @@ -93,38 +93,38 @@ module.exports = React.createClass({ displayName: 'MatrixChat', propTypes: { - config: React.PropTypes.object, - ConferenceHandler: React.PropTypes.any, - onNewScreen: React.PropTypes.func, - registrationUrl: React.PropTypes.string, - enableGuest: React.PropTypes.bool, + config: PropTypes.object, + ConferenceHandler: PropTypes.any, + onNewScreen: PropTypes.func, + registrationUrl: PropTypes.string, + enableGuest: PropTypes.bool, // the queryParams extracted from the [real] query-string of the URI - realQueryParams: React.PropTypes.object, + realQueryParams: PropTypes.object, // the initial queryParams extracted from the hash-fragment of the URI - startingFragmentQueryParams: React.PropTypes.object, + startingFragmentQueryParams: PropTypes.object, // called when we have completed a token login - onTokenLoginCompleted: React.PropTypes.func, + onTokenLoginCompleted: PropTypes.func, // Represents the screen to display as a result of parsing the initial // window.location - initialScreenAfterLogin: React.PropTypes.shape({ - screen: React.PropTypes.string.isRequired, - params: React.PropTypes.object, + initialScreenAfterLogin: PropTypes.shape({ + screen: PropTypes.string.isRequired, + params: PropTypes.object, }), // displayname, if any, to set on the device when logging // in/registering. - defaultDeviceDisplayName: React.PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // A function that makes a registration URL - makeRegistrationUrl: React.PropTypes.func.isRequired, + makeRegistrationUrl: PropTypes.func.isRequired, }, childContextTypes: { - appConfig: React.PropTypes.object, + appConfig: PropTypes.object, }, AuxPanel: { @@ -295,7 +295,6 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - UDEHandler.startListening(); this.focusComposer = false; @@ -361,7 +360,6 @@ module.exports = React.createClass({ componentWillUnmount: function() { Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); - UDEHandler.stopListening(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); }, @@ -1068,10 +1066,10 @@ module.exports = React.createClass({ // this if we are not scrolled up in the view. To find out, delegate to // the timeline panel. If the timeline panel doesn't exist, then we assume // it is safe to reset the timeline. - if (!self.refs.loggedInView) { + if (!self._loggedInView) { return true; } - return self.refs.loggedInView.canResetTimelineInRoom(roomId); + return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId); }); cli.on('sync', function(state, prevState) { @@ -1142,6 +1140,37 @@ module.exports = React.createClass({ room.setBlacklistUnverifiedDevices(blacklistEnabled); } }); + cli.on("crypto.warning", (type) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + switch (type) { + case 'CRYPTO_WARNING_ACCOUNT_MIGRATED': + Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { + title: _t('Cryptography data migrated'), + description: _t( + "A one-off migration of cryptography data has been performed. "+ + "End-to-end encryption will not work if you go back to an older "+ + "version of Riot. If you need to use end-to-end cryptography on "+ + "an older version, log out of Riot first. To retain message history, "+ + "export and re-import your keys.", + ), + }); + break; + case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': + Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { + title: _t('Old cryptography data detected'), + description: _t( + "Data from an older version of Riot has been detected. "+ + "This will have caused end-to-end cryptography to malfunction "+ + "in the older version. End-to-end encrypted messages exchanged "+ + "recently whilst using the older version may not be decryptable "+ + "in this version. This may also cause messages exchanged with this "+ + "version to fail. If you experience problems, log out and back in "+ + "again. To retain message history, export and re-import your keys.", + ), + }); + break; + } + }); }, /** @@ -1398,13 +1427,6 @@ module.exports = React.createClass({ cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => { dis.dispatch({action: 'message_sent'}); }, (err) => { - if (err.name === 'UnknownDeviceError') { - dis.dispatch({ - action: 'unknown_device_error', - err: err, - room: cli.getRoom(roomId), - }); - } dis.dispatch({action: 'message_send_failed'}); }); }, @@ -1466,6 +1488,10 @@ module.exports = React.createClass({ return this.props.makeRegistrationUrl(params); }, + _collectLoggedInView: function(ref) { + this._loggedInView = ref; + }, + render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); @@ -1498,7 +1524,7 @@ module.exports = React.createClass({ */ const LoggedInView = sdk.getComponent('structures.LoggedInView'); return ( -