Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into rxl881/snapshot
This commit is contained in:
commit
f410112983
32 changed files with 1483 additions and 322 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,12 +72,14 @@
|
||||||
"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",
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-css-transition-group": "15.3.2",
|
"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-dom": "^15.4.0",
|
||||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||||
"sanitize-html": "^1.14.1",
|
"sanitize-html": "^1.14.1",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -58,6 +59,7 @@ import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import dis from './dispatcher';
|
import dis from './dispatcher';
|
||||||
|
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//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) {
|
function _setCallListeners(call) {
|
||||||
call.on("error", function(err) {
|
call.on("error", function(err) {
|
||||||
console.error("Call error: %s", err);
|
console.error("Call error: %s", err);
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
call.hangup();
|
if (err.code === 'unknown_devices') {
|
||||||
_setCallState(undefined, call.roomId, "ended");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
});
|
|
||||||
call.on('send_event_error', function(err) {
|
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
|
||||||
if (err.name === "UnknownDeviceError") {
|
title: _t('Call Failed'),
|
||||||
dis.dispatch({
|
description: _t(
|
||||||
action: 'unknown_device_error',
|
"There are unknown devices in this room: "+
|
||||||
err: err,
|
"if you proceed without verifying them, it will be "+
|
||||||
room: MatrixClientPeg.get().getRoom(call.roomId),
|
"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 _onAction(payload) {
|
||||||
function placeCall(newCall) {
|
function placeCall(newCall) {
|
||||||
_setCallListeners(newCall);
|
_setCallListeners(newCall);
|
||||||
_setCallState(newCall, newCall.roomId, "ringback");
|
|
||||||
if (payload.type === 'voice') {
|
if (payload.type === 'voice') {
|
||||||
newCall.placeVoiceCall();
|
newCall.placeVoiceCall();
|
||||||
} else if (payload.type === 'video') {
|
} else if (payload.type === 'video') {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd.
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -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 EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
||||||
import createMatrixClient from './utils/createMatrixClient';
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import MatrixActionCreators from './actions/MatrixActionCreators';
|
||||||
|
|
||||||
interface MatrixClientCreds {
|
interface MatrixClientCreds {
|
||||||
homeserverUrl: string,
|
homeserverUrl: string,
|
||||||
|
@ -68,6 +70,8 @@ class MatrixClientPeg {
|
||||||
|
|
||||||
unset() {
|
unset() {
|
||||||
this.matrixClient = null;
|
this.matrixClient = null;
|
||||||
|
|
||||||
|
MatrixActionCreators.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,6 +112,9 @@ class MatrixClientPeg {
|
||||||
// regardless of errors, start the client. If we did error out, we'll
|
// regardless of errors, start the client. If we did error out, we'll
|
||||||
// just end up doing a full initial /sync.
|
// 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`);
|
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||||
this.get().startClient(opts);
|
this.get().startClient(opts);
|
||||||
console.log(`MatrixClientPeg: MatrixClient started`);
|
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||||
|
|
|
@ -44,13 +44,6 @@ module.exports = {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
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({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
|
@ -60,9 +53,5 @@ module.exports = {
|
||||||
},
|
},
|
||||||
removeFromQueue: function(event) {
|
removeFromQueue: function(event) {
|
||||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_cancelled',
|
|
||||||
event: event,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -76,6 +76,35 @@ class ScalarAuthClient {
|
||||||
return defer.promise;
|
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) {
|
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
||||||
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);
|
||||||
|
|
|
@ -366,6 +366,22 @@ function getWidgets(event, roomId) {
|
||||||
sendResponse(event, widgetStateEvents);
|
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) {
|
function setPlumbingState(event, roomId, status) {
|
||||||
if (typeof status !== 'string') {
|
if (typeof status !== 'string') {
|
||||||
throw new Error('Plumbing state status should be a string');
|
throw new Error('Plumbing state status should be a string');
|
||||||
|
@ -593,6 +609,9 @@ const onMessage = function(event) {
|
||||||
} else if (event.data.action === "get_widgets") {
|
} else if (event.data.action === "get_widgets") {
|
||||||
getWidgets(event, roomId);
|
getWidgets(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
} else if (event.data.action === "get_room_enc_state") {
|
||||||
|
getRoomEncState(event, roomId);
|
||||||
|
return;
|
||||||
} else if (event.data.action === "can_send_event") {
|
} else if (event.data.action === "can_send_event") {
|
||||||
canSendEvent(event, roomId);
|
canSendEvent(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
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:", 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,
|
||||||
|
};
|
34
src/actions/GroupActions.js
Normal file
34
src/actions/GroupActions.js
Normal file
|
@ -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;
|
108
src/actions/MatrixActionCreators.js
Normal file
108
src/actions/MatrixActionCreators.js
Normal file
|
@ -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());
|
||||||
|
},
|
||||||
|
};
|
47
src/actions/TagOrderActions.js
Normal file
47
src/actions/TagOrderActions.js
Normal file
|
@ -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;
|
41
src/actions/actionCreators.js
Normal file
41
src/actions/actionCreators.js
Normal file
|
@ -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});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -53,8 +53,10 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
||||||
|
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||||
|
|
|
@ -18,6 +18,8 @@ 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 { DragDropContext } from 'react-dnd';
|
||||||
|
import HTML5Backend from 'react-dnd-html5-backend';
|
||||||
|
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
|
@ -38,7 +40,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
||||||
*
|
*
|
||||||
* Components mounted below us can access the matrix client via the react context.
|
* Components mounted below us can access the matrix client via the react context.
|
||||||
*/
|
*/
|
||||||
export default React.createClass({
|
const LoggedInView = React.createClass({
|
||||||
displayName: 'LoggedInView',
|
displayName: 'LoggedInView',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -344,3 +346,5 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default DragDropContext(HTML5Backend)(LoggedInView);
|
||||||
|
|
|
@ -40,7 +40,6 @@ require('../../stores/LifecycleStore');
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
|
|
||||||
import createRoom from "../../createRoom";
|
import createRoom from "../../createRoom";
|
||||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
|
||||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
@ -84,7 +83,7 @@ const ONBOARDING_FLOW_STARTERS = [
|
||||||
'view_create_group',
|
'view_create_group',
|
||||||
];
|
];
|
||||||
|
|
||||||
module.exports = React.createClass({
|
export default React.createClass({
|
||||||
// we export this so that the integration tests can use it :-S
|
// we export this so that the integration tests can use it :-S
|
||||||
statics: {
|
statics: {
|
||||||
VIEWS: VIEWS,
|
VIEWS: VIEWS,
|
||||||
|
@ -295,7 +294,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
UDEHandler.startListening();
|
|
||||||
|
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
|
|
||||||
|
@ -361,7 +359,6 @@ module.exports = React.createClass({
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
Lifecycle.stopMatrixClient();
|
Lifecycle.stopMatrixClient();
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
UDEHandler.stopListening();
|
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
@ -1142,6 +1139,37 @@ module.exports = React.createClass({
|
||||||
room.setBlacklistUnverifiedDevices(blacklistEnabled);
|
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 +1426,6 @@ module.exports = React.createClass({
|
||||||
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
|
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
|
||||||
dis.dispatch({action: 'message_sent'});
|
dis.dispatch({action: 'message_sent'});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (err.name === 'UnknownDeviceError') {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'unknown_device_error',
|
|
||||||
err: err,
|
|
||||||
room: cli.getRoom(roomId),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dis.dispatch({action: 'message_send_failed'});
|
dis.dispatch({action: 'message_send_failed'});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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,16 +16,26 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Matrix from 'matrix-js-sdk';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import WhoIsTyping from '../../WhoIsTyping';
|
import WhoIsTyping from '../../WhoIsTyping';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||||
|
import Resend from '../../Resend';
|
||||||
|
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
const STATUS_BAR_EXPANDED_LARGE = 2;
|
const STATUS_BAR_EXPANDED_LARGE = 2;
|
||||||
|
|
||||||
|
function getUnsentMessages(room) {
|
||||||
|
if (!room) { return []; }
|
||||||
|
return room.getPendingEvents().filter(function(ev) {
|
||||||
|
return ev.status === Matrix.EventStatus.NOT_SENT;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomStatusBar',
|
displayName: 'RoomStatusBar',
|
||||||
|
|
||||||
|
@ -35,9 +46,6 @@ module.exports = React.createClass({
|
||||||
// the number of messages which have arrived since we've been scrolled up
|
// the number of messages which have arrived since we've been scrolled up
|
||||||
numUnreadMessages: React.PropTypes.number,
|
numUnreadMessages: React.PropTypes.number,
|
||||||
|
|
||||||
// string to display when there are messages in the room which had errors on send
|
|
||||||
unsentMessageError: React.PropTypes.string,
|
|
||||||
|
|
||||||
// this is true if we are fully scrolled-down, and are looking at
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
// the end of the live timeline.
|
// the end of the live timeline.
|
||||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||||
|
@ -98,12 +106,14 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||||
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
|
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||||
|
|
||||||
this._checkSize();
|
this._checkSize();
|
||||||
},
|
},
|
||||||
|
@ -118,6 +128,7 @@ module.exports = React.createClass({
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("sync", this.onSyncStateChange);
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
|
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -136,6 +147,26 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onResendAllClick: function() {
|
||||||
|
Resend.resendUnsentEvents(this.props.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCancelAllClick: function() {
|
||||||
|
Resend.cancelUnsentEvents(this.props.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onShowDevicesClick: function() {
|
||||||
|
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||||
|
if (room.roomId !== this.props.room.roomId) return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
unsentMessages: getUnsentMessages(this.props.room),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||||
_checkSize: function() {
|
_checkSize: function() {
|
||||||
if (this.props.onVisible && this._getSize()) {
|
if (this.props.onVisible && this._getSize()) {
|
||||||
|
@ -155,7 +186,7 @@ module.exports = React.createClass({
|
||||||
this.props.sentMessageAndIsAlone
|
this.props.sentMessageAndIsAlone
|
||||||
) {
|
) {
|
||||||
return STATUS_BAR_EXPANDED;
|
return STATUS_BAR_EXPANDED;
|
||||||
} else if (this.props.unsentMessageError) {
|
} else if (this.state.unsentMessages.length > 0) {
|
||||||
return STATUS_BAR_EXPANDED_LARGE;
|
return STATUS_BAR_EXPANDED_LARGE;
|
||||||
}
|
}
|
||||||
return STATUS_BAR_HIDDEN;
|
return STATUS_BAR_HIDDEN;
|
||||||
|
@ -241,6 +272,61 @@ module.exports = React.createClass({
|
||||||
return avatars;
|
return avatars;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_getUnsentMessageContent: function() {
|
||||||
|
const unsentMessages = this.state.unsentMessages;
|
||||||
|
if (!unsentMessages.length) return null;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
let content;
|
||||||
|
|
||||||
|
const hasUDE = unsentMessages.some((m) => {
|
||||||
|
return m.error && m.error.name === "UnknownDeviceError";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasUDE) {
|
||||||
|
title = _t("Message not sent due to unknown devices being present");
|
||||||
|
content = _t(
|
||||||
|
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
|
||||||
|
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
unsentMessages.length === 1 &&
|
||||||
|
unsentMessages[0].error &&
|
||||||
|
unsentMessages[0].error.data &&
|
||||||
|
unsentMessages[0].error.data.error
|
||||||
|
) {
|
||||||
|
title = unsentMessages[0].error.data.error;
|
||||||
|
} else {
|
||||||
|
title = _t("Some of your messages have not been sent.");
|
||||||
|
}
|
||||||
|
content = _t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
||||||
|
"You can also select individual messages to resend or cancel.",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
'resendText': (sub) =>
|
||||||
|
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||||
|
'cancelText': (sub) =>
|
||||||
|
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
|
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||||
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
|
{ title }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
|
{ content }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
// return suitable content for the main (text) part of the status bar.
|
// return suitable content for the main (text) part of the status bar.
|
||||||
_getContent: function() {
|
_getContent: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
|
@ -263,28 +349,8 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.unsentMessageError) {
|
if (this.state.unsentMessages.length > 0) {
|
||||||
return (
|
return this._getUnsentMessageContent();
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
|
||||||
{ this.props.unsentMessageError }
|
|
||||||
</div>
|
|
||||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
|
||||||
{
|
|
||||||
_t("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
|
|
||||||
"You can also select individual messages to resend or cancel.",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
'resendText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
|
|
||||||
'cancelText': (sub) =>
|
|
||||||
<a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
|
|
||||||
},
|
|
||||||
) }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unread count trumps who is typing since the unread count is only
|
// unread count trumps who is typing since the unread count is only
|
||||||
|
@ -342,7 +408,6 @@ module.exports = React.createClass({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const content = this._getContent();
|
const content = this._getContent();
|
||||||
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||||
|
|
|
@ -26,7 +26,6 @@ const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
const classNames = require("classnames");
|
const classNames = require("classnames");
|
||||||
const Matrix = require("matrix-js-sdk");
|
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
@ -34,7 +33,6 @@ const ContentMessages = require("../../ContentMessages");
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
const sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
const CallHandler = require('../../CallHandler');
|
const CallHandler = require('../../CallHandler');
|
||||||
const Resend = require("../../Resend");
|
|
||||||
const dis = require("../../dispatcher");
|
const dis = require("../../dispatcher");
|
||||||
const Tinter = require("../../Tinter");
|
const Tinter = require("../../Tinter");
|
||||||
const rate_limited_func = require('../../ratelimitedfunc');
|
const rate_limited_func = require('../../ratelimitedfunc');
|
||||||
|
@ -110,7 +108,6 @@ module.exports = React.createClass({
|
||||||
draggingFile: false,
|
draggingFile: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
searchResults: null,
|
searchResults: null,
|
||||||
unsentMessageError: '',
|
|
||||||
callState: null,
|
callState: null,
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
|
@ -202,7 +199,6 @@ module.exports = React.createClass({
|
||||||
if (initial) {
|
if (initial) {
|
||||||
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
||||||
if (newState.room) {
|
if (newState.room) {
|
||||||
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
|
|
||||||
newState.showApps = this._shouldShowApps(newState.room);
|
newState.showApps = this._shouldShowApps(newState.room);
|
||||||
this._onRoomLoaded(newState.room);
|
this._onRoomLoaded(newState.room);
|
||||||
}
|
}
|
||||||
|
@ -462,11 +458,6 @@ module.exports = React.createClass({
|
||||||
case 'message_send_failed':
|
case 'message_send_failed':
|
||||||
case 'message_sent':
|
case 'message_sent':
|
||||||
this._checkIfAlone(this.state.room);
|
this._checkIfAlone(this.state.room);
|
||||||
// no break; to intentionally fall through
|
|
||||||
case 'message_send_cancelled':
|
|
||||||
this.setState({
|
|
||||||
unsentMessageError: this._getUnsentMessageError(this.state.room),
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case 'picture_snapshot':
|
case 'picture_snapshot':
|
||||||
this.uploadFile(payload.file);
|
this.uploadFile(payload.file);
|
||||||
|
@ -714,35 +705,6 @@ module.exports = React.createClass({
|
||||||
this.setState({isAlone: joinedMembers.length === 1});
|
this.setState({isAlone: joinedMembers.length === 1});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getUnsentMessageError: function(room) {
|
|
||||||
const unsentMessages = this._getUnsentMessages(room);
|
|
||||||
if (!unsentMessages.length) return "";
|
|
||||||
|
|
||||||
if (
|
|
||||||
unsentMessages.length === 1 &&
|
|
||||||
unsentMessages[0].error &&
|
|
||||||
unsentMessages[0].error.data &&
|
|
||||||
unsentMessages[0].error.data.error &&
|
|
||||||
unsentMessages[0].error.name !== "UnknownDeviceError"
|
|
||||||
) {
|
|
||||||
return unsentMessages[0].error.data.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of unsentMessages) {
|
|
||||||
if (!event.error || event.error.name !== "UnknownDeviceError") {
|
|
||||||
return _t("Some of your messages have not been sent.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _t("Message not sent due to unknown devices being present");
|
|
||||||
},
|
|
||||||
|
|
||||||
_getUnsentMessages: function(room) {
|
|
||||||
if (!room) { return []; }
|
|
||||||
return room.getPendingEvents().filter(function(ev) {
|
|
||||||
return ev.status === Matrix.EventStatus.NOT_SENT;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_updateConfCallNotification: function() {
|
_updateConfCallNotification: function() {
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (!room || !this.props.ConferenceHandler) {
|
if (!room || !this.props.ConferenceHandler) {
|
||||||
|
@ -787,14 +749,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onResendAllClick: function() {
|
|
||||||
Resend.resendUnsentEvents(this.state.room);
|
|
||||||
},
|
|
||||||
|
|
||||||
onCancelAllClick: function() {
|
|
||||||
Resend.cancelUnsentEvents(this.state.room);
|
|
||||||
},
|
|
||||||
|
|
||||||
onInviteButtonClick: function() {
|
onInviteButtonClick: function() {
|
||||||
// call AddressPickerDialog
|
// call AddressPickerDialog
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
@ -938,11 +892,7 @@ module.exports = React.createClass({
|
||||||
file, this.state.room.roomId, MatrixClientPeg.get(),
|
file, this.state.room.roomId, MatrixClientPeg.get(),
|
||||||
).done(undefined, (error) => {
|
).done(undefined, (error) => {
|
||||||
if (error.name === "UnknownDeviceError") {
|
if (error.name === "UnknownDeviceError") {
|
||||||
dis.dispatch({
|
// Let the staus bar handle this
|
||||||
action: 'unknown_device_error',
|
|
||||||
err: error,
|
|
||||||
room: this.state.room,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -1574,12 +1524,9 @@ module.exports = React.createClass({
|
||||||
statusBar = <RoomStatusBar
|
statusBar = <RoomStatusBar
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
unsentMessageError={this.state.unsentMessageError}
|
|
||||||
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
|
||||||
sentMessageAndIsAlone={this.state.isAlone}
|
sentMessageAndIsAlone={this.state.isAlone}
|
||||||
hasActiveCall={inCall}
|
hasActiveCall={inCall}
|
||||||
onResendAllClick={this.onResendAllClick}
|
|
||||||
onCancelAllClick={this.onCancelAllClick}
|
|
||||||
onInviteClick={this.onInviteButtonClick}
|
onInviteClick={this.onInviteButtonClick}
|
||||||
onStopWarningClick={this.onStopAloneWarningClick}
|
onStopWarningClick={this.onStopAloneWarningClick}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
|
|
|
@ -17,79 +17,17 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import classNames from 'classnames';
|
|
||||||
import FilterStore from '../../stores/FilterStore';
|
import FilterStore from '../../stores/FilterStore';
|
||||||
import FlairStore from '../../stores/FlairStore';
|
import FlairStore from '../../stores/FlairStore';
|
||||||
|
import TagOrderStore from '../../stores/TagOrderStore';
|
||||||
|
|
||||||
|
import GroupActions from '../../actions/GroupActions';
|
||||||
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
|
|
||||||
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 TagPanel = React.createClass({
|
||||||
displayName: 'TagTile',
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
groupProfile: PropTypes.object,
|
|
||||||
},
|
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState() {
|
|
||||||
return {
|
|
||||||
hover: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onClick: function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'select_tag',
|
|
||||||
tag: this.props.groupProfile.groupId,
|
|
||||||
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
|
||||||
shiftKey: e.shiftKey,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseOver: function() {
|
|
||||||
this.setState({hover: true});
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseOut: function() {
|
|
||||||
this.setState({hover: false});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
|
||||||
const profile = this.props.groupProfile || {};
|
|
||||||
const name = profile.name || profile.groupId;
|
|
||||||
const avatarHeight = 35;
|
|
||||||
|
|
||||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
|
||||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const className = classNames({
|
|
||||||
mx_TagTile: true,
|
|
||||||
mx_TagTile_selected: this.props.selected,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tip = this.state.hover ?
|
|
||||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
|
||||||
<div />;
|
|
||||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
|
||||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
|
||||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
|
||||||
{ tip }
|
|
||||||
</div>
|
|
||||||
</AccessibleButton>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default React.createClass({
|
|
||||||
displayName: 'TagPanel',
|
displayName: 'TagPanel',
|
||||||
|
|
||||||
contextTypes: {
|
contextTypes: {
|
||||||
|
@ -98,7 +36,17 @@ export default React.createClass({
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
joinedGroupProfiles: [],
|
// A list of group profiles for tags that are group IDs. The intention in future
|
||||||
|
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
|
||||||
|
// For now, it suffices to maintain a list of ordered group profiles.
|
||||||
|
orderedGroupTagProfiles: [
|
||||||
|
// {
|
||||||
|
// groupId: '+awesome:foo.bar',{
|
||||||
|
// name: 'My Awesome Community',
|
||||||
|
// avatarUrl: 'mxc://...',
|
||||||
|
// shortDescription: 'Some description...',
|
||||||
|
// },
|
||||||
|
],
|
||||||
selectedTags: [],
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -115,8 +63,23 @@ export default React.createClass({
|
||||||
selectedTags: FilterStore.getSelectedTags(),
|
selectedTags: FilterStore.getSelectedTags(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._fetchJoinedRooms();
|
const orderedTags = TagOrderStore.getOrderedTags() || [];
|
||||||
|
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
|
||||||
|
// XXX: One profile lookup failing will bring the whole lot down
|
||||||
|
Promise.all(orderedGroupTags.map(
|
||||||
|
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
||||||
|
)).then((orderedGroupTagProfiles) => {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
this.setState({orderedGroupTagProfiles});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// This could be done by anything with a matrix client
|
||||||
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -129,7 +92,7 @@ export default React.createClass({
|
||||||
|
|
||||||
_onGroupMyMembership() {
|
_onGroupMyMembership() {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
this._fetchJoinedRooms();
|
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -141,27 +104,21 @@ export default React.createClass({
|
||||||
dis.dispatch({action: 'view_create_group'});
|
dis.dispatch({action: 'view_create_group'});
|
||||||
},
|
},
|
||||||
|
|
||||||
async _fetchJoinedRooms() {
|
onTagTileEndDrag() {
|
||||||
const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups();
|
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
|
||||||
const joinedGroupIds = joinedGroupResponse.groups;
|
|
||||||
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map(
|
|
||||||
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
|
||||||
));
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'all_tags',
|
|
||||||
tags: joinedGroupIds,
|
|
||||||
});
|
|
||||||
this.setState({joinedGroupProfiles});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||||
const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => {
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
return <TagTile
|
|
||||||
|
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
|
||||||
|
return <DNDTagTile
|
||||||
key={groupProfile.groupId + '_' + index}
|
key={groupProfile.groupId + '_' + index}
|
||||||
groupProfile={groupProfile}
|
groupProfile={groupProfile}
|
||||||
selected={this.state.selectedTags.includes(groupProfile.groupId)}
|
selected={this.state.selectedTags.includes(groupProfile.groupId)}
|
||||||
|
onEndDrag={this.onTagTileEndDrag}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
return <div className="mx_TagPanel" onClick={this.onClick}>
|
return <div className="mx_TagPanel" onClick={this.onClick}>
|
||||||
|
@ -174,3 +131,4 @@ export default React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export default TagPanel;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations 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,6 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||||
|
@ -22,6 +24,14 @@ import Resend from '../../../Resend';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
function markAllDevicesKnown(devices) {
|
||||||
|
Object.keys(devices).forEach((userId) => {
|
||||||
|
Object.keys(devices[userId]).map((deviceId) => {
|
||||||
|
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function DeviceListEntry(props) {
|
function DeviceListEntry(props) {
|
||||||
const {userId, device} = props;
|
const {userId, device} = props;
|
||||||
|
|
||||||
|
@ -38,10 +48,10 @@ function DeviceListEntry(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceListEntry.propTypes = {
|
DeviceListEntry.propTypes = {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// deviceinfo
|
// deviceinfo
|
||||||
device: React.PropTypes.object.isRequired,
|
device: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,10 +71,10 @@ function UserUnknownDeviceList(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
UserUnknownDeviceList.propTypes = {
|
UserUnknownDeviceList.propTypes = {
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: PropTypes.string.isRequired,
|
||||||
|
|
||||||
// map from deviceid -> deviceinfo
|
// map from deviceid -> deviceinfo
|
||||||
userDevices: React.PropTypes.object.isRequired,
|
userDevices: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,7 +93,7 @@ function UnknownDeviceList(props) {
|
||||||
|
|
||||||
UnknownDeviceList.propTypes = {
|
UnknownDeviceList.propTypes = {
|
||||||
// map from userid -> deviceid -> deviceinfo
|
// map from userid -> deviceid -> deviceinfo
|
||||||
devices: React.PropTypes.object.isRequired,
|
devices: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,28 +101,63 @@ export default React.createClass({
|
||||||
displayName: 'UnknownDeviceDialog',
|
displayName: 'UnknownDeviceDialog',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
|
|
||||||
// map from userid -> deviceid -> deviceinfo
|
// map from userid -> deviceid -> deviceinfo or null if devices are not yet loaded
|
||||||
devices: React.PropTypes.object.isRequired,
|
devices: PropTypes.object,
|
||||||
onFinished: React.PropTypes.func.isRequired,
|
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// Label for the button that marks all devices known and tries the send again
|
||||||
|
sendAnywayLabel: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// Label for the button that to send the event if you've verified all devices
|
||||||
|
sendLabel: PropTypes.string.isRequired,
|
||||||
|
|
||||||
|
// function to retry the request once all devices are verified / known
|
||||||
|
onSend: PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentWillMount: function() {
|
||||||
// Given we've now shown the user the unknown device, it is no longer
|
MatrixClientPeg.get().on("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||||
// unknown to them. Therefore mark it as 'known'.
|
},
|
||||||
Object.keys(this.props.devices).forEach((userId) => {
|
|
||||||
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
|
||||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// XXX: temporary logging to try to diagnose
|
componentWillUnmount: function() {
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
if (MatrixClientPeg.get()) {
|
||||||
console.log('Opening UnknownDeviceDialog');
|
MatrixClientPeg.get().removeListener("deviceVerificationChanged", this._onDeviceVerificationChanged);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||||
|
if (this.props.devices[userId] && this.props.devices[userId][deviceId]) {
|
||||||
|
// XXX: Mutating props :/
|
||||||
|
this.props.devices[userId][deviceId] = deviceInfo;
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDismissClicked: function() {
|
||||||
|
this.props.onFinished();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSendAnywayClicked: function() {
|
||||||
|
markAllDevicesKnown(this.props.devices);
|
||||||
|
|
||||||
|
this.props.onFinished();
|
||||||
|
this.props.onSend();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSendClicked: function() {
|
||||||
|
this.props.onFinished();
|
||||||
|
this.props.onSend();
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
if (this.props.devices === null) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
let warning;
|
let warning;
|
||||||
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
|
||||||
warning = (
|
warning = (
|
||||||
|
@ -133,15 +178,30 @@ export default React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let haveUnknownDevices = false;
|
||||||
|
Object.keys(this.props.devices).forEach((userId) => {
|
||||||
|
Object.keys(this.props.devices[userId]).map((deviceId) => {
|
||||||
|
const device = this.props.devices[userId][deviceId];
|
||||||
|
if (device.isUnverified() && !device.isKnown()) {
|
||||||
|
haveUnknownDevices = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let sendButton;
|
||||||
|
if (haveUnknownDevices) {
|
||||||
|
sendButton = <button onClick={this._onSendAnywayClicked}>
|
||||||
|
{ this.props.sendAnywayLabel }
|
||||||
|
</button>;
|
||||||
|
} else {
|
||||||
|
sendButton = <button onClick={this._onSendClicked}>
|
||||||
|
{ this.props.sendLabel }
|
||||||
|
</button>;
|
||||||
|
}
|
||||||
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||||
onFinished={() => {
|
onFinished={this.props.onFinished}
|
||||||
// XXX: temporary logging to try to diagnose
|
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
|
||||||
console.log("UnknownDeviceDialog closed by escape");
|
|
||||||
this.props.onFinished();
|
|
||||||
}}
|
|
||||||
title={_t('Room contains unknown devices')}
|
title={_t('Room contains unknown devices')}
|
||||||
>
|
>
|
||||||
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
<GeminiScrollbar autoshow={false} className="mx_Dialog_content">
|
||||||
|
@ -154,21 +214,11 @@ export default React.createClass({
|
||||||
<UnknownDeviceList devices={this.props.devices} />
|
<UnknownDeviceList devices={this.props.devices} />
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
|
{sendButton}
|
||||||
<button className="mx_Dialog_primary" autoFocus={true}
|
<button className="mx_Dialog_primary" autoFocus={true}
|
||||||
onClick={() => {
|
onClick={this._onDismissClicked}
|
||||||
this.props.onFinished();
|
>
|
||||||
Resend.resendUnsentEvents(this.props.room);
|
{_t("Dismiss")}
|
||||||
}}>
|
|
||||||
{ _t("Send anyway") }
|
|
||||||
</button>
|
|
||||||
<button className="mx_Dialog_primary" autoFocus={true}
|
|
||||||
onClick={() => {
|
|
||||||
// XXX: temporary logging to try to diagnose
|
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
|
||||||
console.log("UnknownDeviceDialog closed by OK");
|
|
||||||
this.props.onFinished();
|
|
||||||
}}>
|
|
||||||
OK
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -52,11 +53,13 @@ export default React.createClass({
|
||||||
userId: React.PropTypes.string.isRequired,
|
userId: React.PropTypes.string.isRequired,
|
||||||
// UserId of the entity that added / modified the widget
|
// UserId of the entity that added / modified the widget
|
||||||
creatorUserId: React.PropTypes.string,
|
creatorUserId: React.PropTypes.string,
|
||||||
|
waitForIframeLoad: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return {
|
return {
|
||||||
url: "",
|
url: "",
|
||||||
|
waitForIframeLoad: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -71,17 +74,46 @@ export default React.createClass({
|
||||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||||
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: this.props.waitForIframeLoad, // 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: newProps.widgetPageTitle,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
},
|
},
|
||||||
|
@ -123,6 +155,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();
|
||||||
},
|
},
|
||||||
|
@ -138,7 +172,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;
|
||||||
|
@ -151,7 +185,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);
|
||||||
|
@ -165,6 +199,11 @@ export default React.createClass({
|
||||||
widgetUrl: u.format(),
|
widgetUrl: u.format(),
|
||||||
initialising: false,
|
initialising: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch page title from remote content if not already set
|
||||||
|
if (!this.state.widgetPageTitle && params.url) {
|
||||||
|
this._fetchWidgetTitle(params.url);
|
||||||
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error("Failed to get scalar_token", err);
|
console.error("Failed to get scalar_token", err);
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -175,6 +214,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);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -182,10 +223,14 @@ export default React.createClass({
|
||||||
if (nextProps.url !== this.props.url) {
|
if (nextProps.url !== this.props.url) {
|
||||||
this._getNewState(nextProps);
|
this._getNewState(nextProps);
|
||||||
this.setScalarToken();
|
this.setScalarToken();
|
||||||
} else if (nextProps.show && !this.props.show) {
|
} else if (nextProps.show && !this.props.show && this.props.waitForIframeLoad) {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
|
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
|
||||||
|
this.setState({
|
||||||
|
widgetPageTitle: nextProps.widgetPageTitle,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -268,10 +313,27 @@ 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} url Url to check for title
|
||||||
|
*/
|
||||||
|
_fetchWidgetTitle(url) {
|
||||||
|
this._scalarClient.getScalarPageTitle(url).then((widgetPageTitle) => {
|
||||||
|
if (widgetPageTitle) {
|
||||||
|
this.setState({widgetPageTitle: widgetPageTitle});
|
||||||
|
}
|
||||||
|
}, (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
|
||||||
_deleteWidgetLabel() {
|
_deleteWidgetLabel() {
|
||||||
|
@ -317,6 +379,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;
|
||||||
|
|
||||||
|
@ -332,11 +403,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 = (
|
||||||
|
@ -359,7 +425,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}
|
||||||
|
@ -394,11 +460,24 @@ export default React.createClass({
|
||||||
// Picture snapshot
|
// Picture snapshot
|
||||||
const showPictureSnapshotButton = true; // FIXME - Make this dynamic
|
const showPictureSnapshotButton = true; // FIXME - Make this dynamic
|
||||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
const showPictureSnapshotIcon = 'img/camera_green.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}>
|
||||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||||
<b>{ this.formatAppTileName() }</b>
|
<span className="mx_AppTileMenuBarTitle">
|
||||||
|
<TintableSvgButton
|
||||||
|
src={windowStateIcon}
|
||||||
|
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
title={_t('Minimize apps')}
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
/>
|
||||||
|
<b>{ this.formatAppTileName() }</b>
|
||||||
|
{ this.state.widgetPageTitle && this.state.widgetPageTitle != this.formatAppTileName() && (
|
||||||
|
<span> - { this.state.widgetPageTitle }</span>
|
||||||
|
) }
|
||||||
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Snapshot widget */ }
|
{ /* Snapshot widget */ }
|
||||||
{ showPictureSnapshotButton && <TintableSvgButton
|
{ showPictureSnapshotButton && <TintableSvgButton
|
||||||
|
|
85
src/components/views/elements/DNDTagTile.js
Normal file
85
src/components/views/elements/DNDTagTile.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
/* eslint new-cap: "off" */
|
||||||
|
/*
|
||||||
|
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 { DragSource, DropTarget } from 'react-dnd';
|
||||||
|
|
||||||
|
import TagTile from './TagTile';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import { findDOMNode } from 'react-dom';
|
||||||
|
|
||||||
|
const tagTileSource = {
|
||||||
|
canDrag: function(props, monitor) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
beginDrag: function(props) {
|
||||||
|
// Return the data describing the dragged item
|
||||||
|
return {
|
||||||
|
tag: props.groupProfile.groupId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag: function(props, monitor, component) {
|
||||||
|
const dropResult = monitor.getDropResult();
|
||||||
|
if (!monitor.didDrop() || !dropResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onEndDrag();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagTileTarget = {
|
||||||
|
canDrop(props, monitor) {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
hover(props, monitor, component) {
|
||||||
|
if (!monitor.canDrop()) return;
|
||||||
|
const draggedY = monitor.getClientOffset().y;
|
||||||
|
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||||
|
const targetY = (top + bottom) / 2;
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'order_tag',
|
||||||
|
tag: monitor.getItem().tag,
|
||||||
|
targetTag: props.groupProfile.groupId,
|
||||||
|
// Note: we indicate that the tag should be after the target when
|
||||||
|
// it's being dragged over the top half of the target.
|
||||||
|
after: draggedY < targetY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
drop(props) {
|
||||||
|
// Return the data to be returned by getDropResult
|
||||||
|
return {
|
||||||
|
tag: props.groupProfile.groupId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default
|
||||||
|
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||||
|
connectDropTarget: connect.dropTarget(),
|
||||||
|
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||||
|
connectDragSource: connect.dragSource(),
|
||||||
|
}))((props) => {
|
||||||
|
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||||
|
return connectDropTarget(connectDragSource(
|
||||||
|
<div>
|
||||||
|
<TagTile {...otherProps} />
|
||||||
|
</div>,
|
||||||
|
));
|
||||||
|
}));
|
|
@ -478,7 +478,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
const toggleButton = (
|
const toggleButton = (
|
||||||
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
|
||||||
{ expanded ? 'collapse' : 'expand' }
|
{ expanded ? _t('collapse') : _t('expand') }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
88
src/components/views/elements/TagTile.js
Normal file
88
src/components/views/elements/TagTile.js
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
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 React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'TagTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
groupProfile: PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
|
contextTypes: {
|
||||||
|
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'select_tag',
|
||||||
|
tag: this.props.groupProfile.groupId,
|
||||||
|
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||||
|
shiftKey: e.shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOver: function() {
|
||||||
|
this.setState({hover: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOut: function() {
|
||||||
|
this.setState({hover: false});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||||
|
const profile = this.props.groupProfile || {};
|
||||||
|
const name = profile.name || profile.groupId;
|
||||||
|
const avatarHeight = 35;
|
||||||
|
|
||||||
|
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||||
|
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const className = classNames({
|
||||||
|
mx_TagTile: true,
|
||||||
|
mx_TagTile_selected: this.props.selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tip = this.state.hover ?
|
||||||
|
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||||
|
<div />;
|
||||||
|
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||||
|
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||||
|
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||||
|
{ tip }
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>;
|
||||||
|
},
|
||||||
|
});
|
|
@ -253,7 +253,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>Addresses</h3>
|
<h3>{ _t('Addresses') }</h3>
|
||||||
<div className="mx_RoomSettings_aliasLabel">
|
<div className="mx_RoomSettings_aliasLabel">
|
||||||
{ _t('The main address for this room is') }: { canonical_alias_section }
|
{ _t('The main address for this room is') }: { canonical_alias_section }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -133,14 +133,17 @@ module.exports = React.createClass({
|
||||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app.id = appId;
|
||||||
|
app.name = app.name || app.type;
|
||||||
|
|
||||||
if (app.data) {
|
if (app.data) {
|
||||||
Object.keys(app.data).forEach((key) => {
|
Object.keys(app.data).forEach((key) => {
|
||||||
params['$' + key] = app.data[key];
|
params['$' + key] = app.data[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.id = appId;
|
|
||||||
app.name = app.name || app.type;
|
|
||||||
app.url = this.encodeUri(app.url, params);
|
app.url = this.encodeUri(app.url, params);
|
||||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||||
|
|
||||||
|
@ -224,6 +227,8 @@ module.exports = React.createClass({
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
show={this.props.showApps}
|
show={this.props.showApps}
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={app.creatorUserId}
|
||||||
|
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||||
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
/>);
|
/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -74,13 +74,6 @@ function onSendMessageFailed(err, room) {
|
||||||
// XXX: temporary logging to try to diagnose
|
// XXX: temporary logging to try to diagnose
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||||
if (err.name === "UnknownDeviceError") {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'unknown_device_error',
|
|
||||||
err: err,
|
|
||||||
room: room,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_send_failed',
|
action: 'message_send_failed',
|
||||||
});
|
});
|
||||||
|
|
104
src/cryptodevices.js
Normal file
104
src/cryptodevices.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
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 Resend from './Resend';
|
||||||
|
import sdk from './index';
|
||||||
|
import Modal from './Modal';
|
||||||
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all crypto devices in a room that are marked neither known
|
||||||
|
* nor verified.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient A MatrixClient
|
||||||
|
* @param {Room} room js-sdk room object representing the room
|
||||||
|
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||||
|
* module:crypto~DeviceInfo|DeviceInfo}.
|
||||||
|
*/
|
||||||
|
export function getUnknownDevicesForRoom(matrixClient, room) {
|
||||||
|
const roomMembers = room.getJoinedMembers().map((m) => {
|
||||||
|
return m.userId;
|
||||||
|
});
|
||||||
|
return matrixClient.downloadKeys(roomMembers, false).then((devices) => {
|
||||||
|
const unknownDevices = {};
|
||||||
|
// This is all devices in this room, so find the unknown ones.
|
||||||
|
Object.keys(devices).forEach((userId) => {
|
||||||
|
Object.keys(devices[userId]).map((deviceId) => {
|
||||||
|
const device = devices[userId][deviceId];
|
||||||
|
|
||||||
|
if (device.isUnverified() && !device.isKnown()) {
|
||||||
|
if (unknownDevices[userId] === undefined) {
|
||||||
|
unknownDevices[userId] = {};
|
||||||
|
}
|
||||||
|
unknownDevices[userId][deviceId] = device;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return unknownDevices;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the UnknownDeviceDialog for a given room. The dialog will inform the user
|
||||||
|
* that messages they sent to this room have not been sent due to unknown devices
|
||||||
|
* being present.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient A MatrixClient
|
||||||
|
* @param {Room} room js-sdk room object representing the room
|
||||||
|
*/
|
||||||
|
export function showUnknownDeviceDialogForMessages(matrixClient, room) {
|
||||||
|
getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => {
|
||||||
|
const onSendClicked = () => {
|
||||||
|
Resend.resendUnsentEvents(room);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
|
Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, {
|
||||||
|
room: room,
|
||||||
|
devices: unknownDevices,
|
||||||
|
sendAnywayLabel: _t("Send anyway"),
|
||||||
|
sendLabel: _t("Send"),
|
||||||
|
onSend: onSendClicked,
|
||||||
|
}, 'mx_Dialog_unknownDevice');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the UnknownDeviceDialog for a given room. The dialog will inform the user
|
||||||
|
* that a call they tried to place or answer in the room couldn't be placed or
|
||||||
|
* answered due to unknown devices being present.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient A MatrixClient
|
||||||
|
* @param {Room} room js-sdk room object representing the room
|
||||||
|
* @param {func} sendAnyway Function called when the 'call anyway' or 'call'
|
||||||
|
* button is pressed. This should attempt to place or answer the call again.
|
||||||
|
* @param {string} sendAnywayLabel Label for the button displayed to retry the call
|
||||||
|
* when unknown devices are still present (eg. "Call Anyway")
|
||||||
|
* @param {string} sendLabel Label for the button displayed to retry the call
|
||||||
|
* after all devices have been verified (eg. "Call")
|
||||||
|
*/
|
||||||
|
export function showUnknownDeviceDialogForCalls(matrixClient, room, sendAnyway, sendAnywayLabel, sendLabel) {
|
||||||
|
getUnknownDevicesForRoom(matrixClient, room).then((unknownDevices) => {
|
||||||
|
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
|
||||||
|
Modal.createTrackedDialog('Unknown Device Dialog', '', UnknownDeviceDialog, {
|
||||||
|
room: room,
|
||||||
|
devices: unknownDevices,
|
||||||
|
sendAnywayLabel: sendAnywayLabel,
|
||||||
|
sendLabel: sendLabel,
|
||||||
|
onSend: sendAnyway,
|
||||||
|
}, 'mx_Dialog_unknownDevice');
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 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.
|
||||||
|
@ -20,14 +21,24 @@ const flux = require("flux");
|
||||||
|
|
||||||
class MatrixDispatcher extends flux.Dispatcher {
|
class MatrixDispatcher extends flux.Dispatcher {
|
||||||
/**
|
/**
|
||||||
* @param {Object} payload Required. The payload to dispatch.
|
* @param {Object|function} payload Required. The payload to dispatch.
|
||||||
* Must contain at least an 'action' key.
|
* If an Object, must contain at least an 'action' key.
|
||||||
|
* If a function, must have the signature (dispatch) => {...}.
|
||||||
* @param {boolean=} sync Optional. Pass true to dispatch
|
* @param {boolean=} sync Optional. Pass true to dispatch
|
||||||
* synchronously. This is useful for anything triggering
|
* synchronously. This is useful for anything triggering
|
||||||
* an operation that the browser requires user interaction
|
* an operation that the browser requires user interaction
|
||||||
* for.
|
* for.
|
||||||
*/
|
*/
|
||||||
dispatch(payload, sync) {
|
dispatch(payload, sync) {
|
||||||
|
// Allow for asynchronous dispatching by accepting payloads that have the
|
||||||
|
// type `function (dispatch) {...}`
|
||||||
|
if (typeof payload === 'function') {
|
||||||
|
payload((action) => {
|
||||||
|
this.dispatch(action, sync);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sync) {
|
if (sync) {
|
||||||
super.dispatch(payload);
|
super.dispatch(payload);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,6 +2,13 @@
|
||||||
"This email address is already in use": "This email address is already in use",
|
"This email address is already in use": "This email address is already in use",
|
||||||
"This phone number is already in use": "This phone number is already in use",
|
"This phone number is already in use": "This phone number is already in use",
|
||||||
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
|
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
|
||||||
|
"Call Failed": "Call Failed",
|
||||||
|
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.",
|
||||||
|
"Review Devices": "Review Devices",
|
||||||
|
"Call Anyway": "Call Anyway",
|
||||||
|
"Answer Anyway": "Answer Anyway",
|
||||||
|
"Call": "Call",
|
||||||
|
"Answer": "Answer",
|
||||||
"Call Timeout": "Call Timeout",
|
"Call Timeout": "Call Timeout",
|
||||||
"The remote side failed to pick up": "The remote side failed to pick up",
|
"The remote side failed to pick up": "The remote side failed to pick up",
|
||||||
"Unable to capture screen": "Unable to capture screen",
|
"Unable to capture screen": "Unable to capture screen",
|
||||||
|
@ -156,6 +163,8 @@
|
||||||
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
"%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing",
|
||||||
"Failure to create room": "Failure to create room",
|
"Failure to create room": "Failure to create room",
|
||||||
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
|
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
|
||||||
|
"Send anyway": "Send anyway",
|
||||||
|
"Send": "Send",
|
||||||
"Unnamed Room": "Unnamed Room",
|
"Unnamed Room": "Unnamed Room",
|
||||||
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
||||||
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||||
|
@ -448,6 +457,7 @@
|
||||||
"not specified": "not specified",
|
"not specified": "not specified",
|
||||||
"not set": "not set",
|
"not set": "not set",
|
||||||
"Remote addresses for this room:": "Remote addresses for this room:",
|
"Remote addresses for this room:": "Remote addresses for this room:",
|
||||||
|
"Addresses": "Addresses",
|
||||||
"The main address for this room is": "The main address for this room is",
|
"The main address for this room is": "The main address for this room is",
|
||||||
"Local addresses for this room:": "Local addresses for this room:",
|
"Local addresses for this room:": "Local addresses for this room:",
|
||||||
"This room has no local addresses": "This room has no local addresses",
|
"This room has no local addresses": "This room has no local addresses",
|
||||||
|
@ -612,6 +622,8 @@
|
||||||
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
|
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
|
||||||
"%(items)s and %(count)s others|one": "%(items)s and one other",
|
"%(items)s and %(count)s others|one": "%(items)s and one other",
|
||||||
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
|
||||||
|
"collapse": "collapse",
|
||||||
|
"expand": "expand",
|
||||||
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
|
||||||
"Custom level": "Custom level",
|
"Custom level": "Custom level",
|
||||||
"Room directory": "Room directory",
|
"Room directory": "Room directory",
|
||||||
|
@ -692,7 +704,6 @@
|
||||||
"Room contains unknown devices": "Room contains unknown devices",
|
"Room contains unknown devices": "Room contains unknown devices",
|
||||||
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
|
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
|
||||||
"Unknown devices": "Unknown devices",
|
"Unknown devices": "Unknown devices",
|
||||||
"Send anyway": "Send anyway",
|
|
||||||
"Private Chat": "Private Chat",
|
"Private Chat": "Private Chat",
|
||||||
"Public Chat": "Public Chat",
|
"Public Chat": "Public Chat",
|
||||||
"Custom": "Custom",
|
"Custom": "Custom",
|
||||||
|
@ -748,6 +759,10 @@
|
||||||
"Failed to leave room": "Failed to leave room",
|
"Failed to leave room": "Failed to leave room",
|
||||||
"Signed Out": "Signed Out",
|
"Signed Out": "Signed Out",
|
||||||
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
||||||
|
"Cryptography data migrated": "Cryptography data migrated",
|
||||||
|
"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.": "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.",
|
||||||
|
"Old cryptography data detected": "Old cryptography data detected",
|
||||||
|
"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.": "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.",
|
||||||
"Logout": "Logout",
|
"Logout": "Logout",
|
||||||
"Your Communities": "Your Communities",
|
"Your Communities": "Your Communities",
|
||||||
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||||
|
@ -757,17 +772,19 @@
|
||||||
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.",
|
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.",
|
||||||
"You have no visible notifications": "You have no visible notifications",
|
"You have no visible notifications": "You have no visible notifications",
|
||||||
"Scroll to bottom of page": "Scroll to bottom of page",
|
"Scroll to bottom of page": "Scroll to bottom of page",
|
||||||
|
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
||||||
|
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.": "<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.",
|
||||||
|
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
|
||||||
|
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
||||||
|
"Warning": "Warning",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
|
|
||||||
"%(count)s new messages|other": "%(count)s new messages",
|
"%(count)s new messages|other": "%(count)s new messages",
|
||||||
"%(count)s new messages|one": "%(count)s new message",
|
"%(count)s new messages|one": "%(count)s new message",
|
||||||
"Active call": "Active call",
|
"Active call": "Active call",
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
"Some of your messages have not been sent.": "Some of your messages have not been sent.",
|
|
||||||
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
|
||||||
"Failed to upload file": "Failed to upload file",
|
"Failed to upload file": "Failed to upload file",
|
||||||
"Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big",
|
"Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big",
|
||||||
"Search failed": "Search failed",
|
"Search failed": "Search failed",
|
||||||
|
|
137
src/stores/TagOrderStore.js
Normal file
137
src/stores/TagOrderStore.js
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
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 {Store} from 'flux/utils';
|
||||||
|
import dis from '../dispatcher';
|
||||||
|
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
orderedTags: null,
|
||||||
|
orderedTagsAccountData: null,
|
||||||
|
hasSynced: false,
|
||||||
|
joinedGroupIds: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for storing application state for ordering tags in the TagPanel.
|
||||||
|
*/
|
||||||
|
class TagOrderStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(dis);
|
||||||
|
|
||||||
|
// Initialise state
|
||||||
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setState(newState) {
|
||||||
|
this._state = Object.assign(this._state, newState);
|
||||||
|
this.__emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
__onDispatch(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
// Initialise state after initial sync
|
||||||
|
case 'MatrixActions.sync': {
|
||||||
|
if (!(payload.prevState === 'PREPARED' && payload.state === 'SYNCING')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering');
|
||||||
|
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
|
||||||
|
this._setState({
|
||||||
|
orderedTagsAccountData: tagOrderingEventContent.tags || null,
|
||||||
|
hasSynced: true,
|
||||||
|
});
|
||||||
|
this._updateOrderedTags();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Get ordering from account data
|
||||||
|
case 'MatrixActions.accountData': {
|
||||||
|
if (payload.event_type !== 'im.vector.web.tag_ordering') break;
|
||||||
|
this._setState({
|
||||||
|
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
|
||||||
|
});
|
||||||
|
this._updateOrderedTags();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Initialise the state such that if account data is unset, default to joined groups
|
||||||
|
case 'GroupActions.fetchJoinedGroups.success': {
|
||||||
|
this._setState({
|
||||||
|
joinedGroupIds: payload.result.groups.sort(), // Sort lexically
|
||||||
|
hasFetchedJoinedGroups: true,
|
||||||
|
});
|
||||||
|
this._updateOrderedTags();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag
|
||||||
|
case 'order_tag': {
|
||||||
|
if (!this._state.orderedTags ||
|
||||||
|
!payload.tag ||
|
||||||
|
!payload.targetTag ||
|
||||||
|
payload.tag === payload.targetTag
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const tags = this._state.orderedTags;
|
||||||
|
|
||||||
|
let orderedTags = tags.filter((t) => t !== payload.tag);
|
||||||
|
const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0);
|
||||||
|
orderedTags = [
|
||||||
|
...orderedTags.slice(0, newIndex),
|
||||||
|
payload.tag,
|
||||||
|
...orderedTags.slice(newIndex),
|
||||||
|
];
|
||||||
|
this._setState({orderedTags});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'on_logged_out': {
|
||||||
|
// Reset state without pushing an update to the view, which generally assumes that
|
||||||
|
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
|
||||||
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateOrderedTags() {
|
||||||
|
this._setState({
|
||||||
|
orderedTags:
|
||||||
|
this._state.hasSynced &&
|
||||||
|
this._state.hasFetchedJoinedGroups ?
|
||||||
|
this._mergeGroupsAndTags() : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_mergeGroupsAndTags() {
|
||||||
|
const groupIds = this._state.joinedGroupIds || [];
|
||||||
|
const tags = this._state.orderedTagsAccountData || [];
|
||||||
|
|
||||||
|
const tagsToKeep = tags.filter(
|
||||||
|
(t) => t[0] !== '+' || groupIds.includes(t),
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupIdsToAdd = groupIds.filter(
|
||||||
|
(groupId) => !tags.includes(groupId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return tagsToKeep.concat(groupIdsToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrderedTags() {
|
||||||
|
return this._state.orderedTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.singletonTagOrderStore === undefined) {
|
||||||
|
global.singletonTagOrderStore = new TagOrderStore();
|
||||||
|
}
|
||||||
|
export default global.singletonTagOrderStore;
|
Loading…
Reference in a new issue