Merge branch 'develop' into t3chguy/hide_left_chats_memberinfo
This commit is contained in:
77
src/ActiveRoomObserver.js
Normal file
77
src/ActiveRoomObserver.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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 RoomViewStore from './stores/RoomViewStore';
|
||||
|
||||
/**
|
||||
* Consumes changes from the RoomViewStore and notifies specific things
|
||||
* about when the active room changes. Unlike listening for RoomViewStore
|
||||
* changes, you can subscribe to only changes relevant to a particular
|
||||
* room.
|
||||
*
|
||||
* TODO: If we introduce an observer for something else, factor out
|
||||
* the adding / removing of listeners & emitting into a common class.
|
||||
*/
|
||||
class ActiveRoomObserver {
|
||||
constructor() {
|
||||
this._listeners = {};
|
||||
|
||||
this._activeRoomId = RoomViewStore.getRoomId();
|
||||
// TODO: We could self-destruct when the last listener goes away, or at least
|
||||
// stop listening.
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
|
||||
}
|
||||
|
||||
addListener(roomId, listener) {
|
||||
if (!this._listeners[roomId]) this._listeners[roomId] = [];
|
||||
this._listeners[roomId].push(listener);
|
||||
}
|
||||
|
||||
removeListener(roomId, listener) {
|
||||
if (this._listeners[roomId]) {
|
||||
const i = this._listeners[roomId].indexOf(listener);
|
||||
if (i > -1) {
|
||||
this._listeners[roomId].splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
console.warn("Unregistering unrecognised listener (roomId=" + roomId + ")");
|
||||
}
|
||||
}
|
||||
|
||||
_emit(roomId) {
|
||||
if (!this._listeners[roomId]) return;
|
||||
|
||||
for (const l of this._listeners[roomId]) {
|
||||
l.call();
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate() {
|
||||
// emit for the old room ID
|
||||
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||
|
||||
// update our cache
|
||||
this._activeRoomId = RoomViewStore.getRoomId();
|
||||
|
||||
// and emit for the new one
|
||||
if (this._activeRoomId) this._emit(this._activeRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
if (global.mx_ActiveRoomObserver === undefined) {
|
||||
global.mx_ActiveRoomObserver = new ActiveRoomObserver();
|
||||
}
|
||||
export default global.mx_ActiveRoomObserver;
|
||||
@@ -19,7 +19,7 @@ import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
|
||||
function getRedactedUrl() {
|
||||
const redactedHash = window.location.hash.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
// hardcoded url to make piwik happy
|
||||
return 'https://riot.im/app/' + redactedHash;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ export default class BasePlatform {
|
||||
|
||||
isElectron(): boolean { return false; }
|
||||
|
||||
setupScreenSharingForIframe() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the application, without neccessarily reloading
|
||||
* any application code
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -52,34 +53,33 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import Modal from './Modal';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||
|
||||
global.mxCalls = {
|
||||
//room_id: MatrixCall
|
||||
};
|
||||
var calls = global.mxCalls;
|
||||
var ConferenceHandler = null;
|
||||
const calls = global.mxCalls;
|
||||
let ConferenceHandler = null;
|
||||
|
||||
var audioPromises = {};
|
||||
const audioPromises = {};
|
||||
|
||||
function play(audioId) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
var audio = document.getElementById(audioId);
|
||||
const audio = document.getElementById(audioId);
|
||||
if (audio) {
|
||||
if (audioPromises[audioId]) {
|
||||
audioPromises[audioId] = audioPromises[audioId].then(()=>{
|
||||
audio.load();
|
||||
return audio.play();
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
audioPromises[audioId] = audio.play();
|
||||
}
|
||||
}
|
||||
@@ -88,31 +88,65 @@ function play(audioId) {
|
||||
function pause(audioId) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
var audio = document.getElementById(audioId);
|
||||
const audio = document.getElementById(audioId);
|
||||
if (audio) {
|
||||
if (audioPromises[audioId]) {
|
||||
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// pause doesn't actually return a promise, but might as well do this for symmetry with play();
|
||||
audioPromises[audioId] = audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _reAttemptCall(call) {
|
||||
if (call.direction === 'outbound') {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
room_id: call.roomId,
|
||||
type: call.type,
|
||||
});
|
||||
} else {
|
||||
call.answer();
|
||||
}
|
||||
}
|
||||
|
||||
function _setCallListeners(call) {
|
||||
call.on("error", function(err) {
|
||||
console.error("Call error: %s", err);
|
||||
console.error(err.stack);
|
||||
call.hangup();
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
});
|
||||
call.on('send_event_error', function(err) {
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: MatrixClientPeg.get().getRoom(call.roomId),
|
||||
if (err.code === 'unknown_devices') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
Modal.createTrackedDialog('Call Failed', '', QuestionDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: _t(
|
||||
"There are unknown devices in this room: "+
|
||||
"if you proceed without verifying them, it will be "+
|
||||
"possible for someone to eavesdrop on your call."
|
||||
),
|
||||
button: _t('Review Devices'),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
const room = MatrixClientPeg.get().getRoom(call.roomId);
|
||||
showUnknownDeviceDialogForCalls(
|
||||
MatrixClientPeg.get(),
|
||||
room,
|
||||
() => {
|
||||
_reAttemptCall(call);
|
||||
},
|
||||
call.direction === 'outbound' ? _t("Call Anyway") : _t("Answer Anyway"),
|
||||
call.direction === 'outbound' ? _t("Call") : _t("Answer"),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -125,38 +159,32 @@ function _setCallListeners(call) {
|
||||
if (newState === "ringing") {
|
||||
_setCallState(call, call.roomId, "ringing");
|
||||
pause("ringbackAudio");
|
||||
}
|
||||
else if (newState === "invite_sent") {
|
||||
} else if (newState === "invite_sent") {
|
||||
_setCallState(call, call.roomId, "ringback");
|
||||
play("ringbackAudio");
|
||||
}
|
||||
else if (newState === "ended" && oldState === "connected") {
|
||||
} else if (newState === "ended" && oldState === "connected") {
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
pause("ringbackAudio");
|
||||
play("callendAudio");
|
||||
}
|
||||
else if (newState === "ended" && oldState === "invite_sent" &&
|
||||
} else if (newState === "ended" && oldState === "invite_sent" &&
|
||||
(call.hangupParty === "remote" ||
|
||||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
||||
)) {
|
||||
_setCallState(call, call.roomId, "busy");
|
||||
pause("ringbackAudio");
|
||||
play("busyAudio");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||
title: _t('Call Timeout'),
|
||||
description: _t('The remote side failed to pick up') + '.',
|
||||
});
|
||||
}
|
||||
else if (oldState === "invite_sent") {
|
||||
} else if (oldState === "invite_sent") {
|
||||
_setCallState(call, call.roomId, "stop_ringback");
|
||||
pause("ringbackAudio");
|
||||
}
|
||||
else if (oldState === "ringing") {
|
||||
} else if (oldState === "ringing") {
|
||||
_setCallState(call, call.roomId, "stop_ringing");
|
||||
pause("ringbackAudio");
|
||||
}
|
||||
else if (newState === "connected") {
|
||||
} else if (newState === "connected") {
|
||||
_setCallState(call, call.roomId, "connected");
|
||||
pause("ringbackAudio");
|
||||
}
|
||||
@@ -165,14 +193,13 @@ function _setCallListeners(call) {
|
||||
|
||||
function _setCallState(call, roomId, status) {
|
||||
console.log(
|
||||
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-")
|
||||
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-"),
|
||||
);
|
||||
calls[roomId] = call;
|
||||
|
||||
if (status === "ringing") {
|
||||
play("ringAudio");
|
||||
}
|
||||
else if (call && call.call_state === "ringing") {
|
||||
} else if (call && call.call_state === "ringing") {
|
||||
pause("ringAudio");
|
||||
}
|
||||
|
||||
@@ -189,17 +216,14 @@ function _setCallState(call, roomId, status) {
|
||||
function _onAction(payload) {
|
||||
function placeCall(newCall) {
|
||||
_setCallListeners(newCall);
|
||||
_setCallState(newCall, newCall.roomId, "ringback");
|
||||
if (payload.type === 'voice') {
|
||||
newCall.placeVoiceCall();
|
||||
}
|
||||
else if (payload.type === 'video') {
|
||||
} else if (payload.type === 'video') {
|
||||
newCall.placeVideoCall(
|
||||
payload.remote_element,
|
||||
payload.local_element
|
||||
payload.local_element,
|
||||
);
|
||||
}
|
||||
else if (payload.type === 'screensharing') {
|
||||
} else if (payload.type === 'screensharing') {
|
||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||
if (screenCapErrorString) {
|
||||
_setCallState(undefined, newCall.roomId, "ended");
|
||||
@@ -213,10 +237,9 @@ function _onAction(payload) {
|
||||
}
|
||||
newCall.placeScreenSharingCall(
|
||||
payload.remote_element,
|
||||
payload.local_element
|
||||
payload.local_element,
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.error("Unknown conf call type: %s", payload.type);
|
||||
}
|
||||
}
|
||||
@@ -255,21 +278,17 @@ function _onAction(payload) {
|
||||
description: _t('You cannot place a call with yourself.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (members.length === 2) {
|
||||
} else if (members.length === 2) {
|
||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
|
||||
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
|
||||
});
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||
placeCall(call);
|
||||
}
|
||||
else { // > 2
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -280,15 +299,13 @@ function _onAction(payload) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||
description: _t('Conference calls are not supported in this client'),
|
||||
});
|
||||
}
|
||||
else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
} else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
}
|
||||
else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||
} else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||
// Conference calls are implemented by sending the media to central
|
||||
// server which combines the audio from all the participants together
|
||||
// into a single stream. This is incompatible with end-to-end encryption
|
||||
@@ -299,16 +316,15 @@ function _onAction(payload) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||
});
|
||||
}
|
||||
else {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description: _t('Conference calling is in development and may not be reliable.'),
|
||||
onFinished: confirm=>{
|
||||
onFinished: (confirm)=>{
|
||||
if (confirm) {
|
||||
ConferenceHandler.createNewMatrixCall(
|
||||
MatrixClientPeg.get(), payload.room_id
|
||||
MatrixClientPeg.get(), payload.room_id,
|
||||
).done(function(call) {
|
||||
placeCall(call);
|
||||
}, function(err) {
|
||||
@@ -357,7 +373,7 @@ function _onAction(payload) {
|
||||
_setCallState(calls[payload.room_id], payload.room_id, "connected");
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: payload.room_id
|
||||
room_id: payload.room_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -368,9 +384,9 @@ if (!global.mxCallHandler) {
|
||||
dis.register(_onAction);
|
||||
}
|
||||
|
||||
var callHandler = {
|
||||
const callHandler = {
|
||||
getCallForRoom: function(roomId) {
|
||||
var call = module.exports.getCall(roomId);
|
||||
let call = module.exports.getCall(roomId);
|
||||
if (call) return call;
|
||||
|
||||
if (ConferenceHandler) {
|
||||
@@ -386,8 +402,8 @@ var callHandler = {
|
||||
},
|
||||
|
||||
getAnyActiveCall: function() {
|
||||
var roomsWithCalls = Object.keys(calls);
|
||||
for (var i = 0; i < roomsWithCalls.length; i++) {
|
||||
const roomsWithCalls = Object.keys(calls);
|
||||
for (let i = 0; i < roomsWithCalls.length; i++) {
|
||||
if (calls[roomsWithCalls[i]] &&
|
||||
calls[roomsWithCalls[i]].call_state !== "ended") {
|
||||
return calls[roomsWithCalls[i]];
|
||||
@@ -402,7 +418,7 @@ var callHandler = {
|
||||
|
||||
getConferenceHandler: function() {
|
||||
return ConferenceHandler;
|
||||
}
|
||||
},
|
||||
};
|
||||
// Only things in here which actually need to be global are the
|
||||
// calls list (done separately) and making sure we only register
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
export default {
|
||||
getDevices: function() {
|
||||
@@ -43,22 +43,20 @@ export default {
|
||||
},
|
||||
|
||||
loadDevices: function() {
|
||||
// this.getDevices().then((devices) => {
|
||||
const localSettings = UserSettingsStore.getLocalSettings();
|
||||
// // if deviceId is not found, automatic fallback is in spec
|
||||
// // recall previously stored inputs if any
|
||||
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
|
||||
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
|
||||
// });
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioInput(deviceId);
|
||||
},
|
||||
|
||||
setVideoInput: function(deviceId) {
|
||||
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallVideoInput(deviceId);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export default class ComposerHistoryManager {
|
||||
|
||||
// TODO: Performance issues?
|
||||
let item;
|
||||
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||
this.history.push(
|
||||
Object.assign(new HistoryItem(), JSON.parse(item)),
|
||||
);
|
||||
|
||||
@@ -17,14 +17,14 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
var extend = require('./extend');
|
||||
var dis = require('./dispatcher');
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
var sdk = require('./index');
|
||||
const extend = require('./extend');
|
||||
const dis = require('./dispatcher');
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
const sdk = require('./index');
|
||||
import { _t } from './languageHandler';
|
||||
var Modal = require('./Modal');
|
||||
const Modal = require('./Modal');
|
||||
|
||||
var encrypt = require("browser-encrypt-attachment");
|
||||
const encrypt = require("browser-encrypt-attachment");
|
||||
|
||||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
require("blueimp-canvas-to-blob");
|
||||
@@ -54,8 +54,8 @@ const MAX_HEIGHT = 600;
|
||||
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
const deferred = Promise.defer();
|
||||
|
||||
var targetWidth = inputWidth;
|
||||
var targetHeight = inputHeight;
|
||||
let targetWidth = inputWidth;
|
||||
let targetHeight = inputHeight;
|
||||
if (targetHeight > MAX_HEIGHT) {
|
||||
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
|
||||
targetHeight = MAX_HEIGHT;
|
||||
@@ -81,7 +81,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
|
||||
w: inputWidth,
|
||||
h: inputHeight,
|
||||
},
|
||||
thumbnail: thumbnail
|
||||
thumbnail: thumbnail,
|
||||
});
|
||||
}, mimeType);
|
||||
|
||||
@@ -99,23 +99,17 @@ function loadImageElement(imageFile) {
|
||||
|
||||
// Load the file into an html element
|
||||
const img = document.createElement("img");
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
img.src = objectUrl;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
img.src = e.target.result;
|
||||
|
||||
// Once ready, create a thumbnail
|
||||
img.onload = function() {
|
||||
deferred.resolve(img);
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
// Once ready, create a thumbnail
|
||||
img.onload = function() {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
deferred.resolve(img);
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
@@ -129,12 +123,12 @@ function loadImageElement(imageFile) {
|
||||
* @return {Promise} A promise that resolves with the attachment info.
|
||||
*/
|
||||
function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||
var thumbnailType = "image/png";
|
||||
let thumbnailType = "image/png";
|
||||
if (imageFile.type == "image/jpeg") {
|
||||
thumbnailType = "image/jpeg";
|
||||
}
|
||||
|
||||
var imageInfo;
|
||||
let imageInfo;
|
||||
return loadImageElement(imageFile).then(function(img) {
|
||||
return createThumbnail(img, img.width, img.height, thumbnailType);
|
||||
}).then(function(result) {
|
||||
@@ -191,7 +185,7 @@ function loadVideoElement(videoFile) {
|
||||
function infoForVideoFile(matrixClient, roomId, videoFile) {
|
||||
const thumbnailType = "image/jpeg";
|
||||
|
||||
var videoInfo;
|
||||
let videoInfo;
|
||||
return loadVideoElement(videoFile).then(function(video) {
|
||||
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
|
||||
}).then(function(result) {
|
||||
@@ -286,7 +280,7 @@ class ContentMessages {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
size: file.size,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// if we have a mime type for the file, add it to the message metadata
|
||||
@@ -297,10 +291,10 @@ class ContentMessages {
|
||||
const def = Promise.defer();
|
||||
if (file.type.indexOf('image/') == 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
|
||||
extend(content.info, imageInfo);
|
||||
def.resolve();
|
||||
}, error=>{
|
||||
}, (error)=>{
|
||||
console.error(error);
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
@@ -310,10 +304,10 @@ class ContentMessages {
|
||||
def.resolve();
|
||||
} else if (file.type.indexOf('video/') == 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(matrixClient, roomId, file).then(videoInfo=>{
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
|
||||
extend(content.info, videoInfo);
|
||||
def.resolve();
|
||||
}, error=>{
|
||||
}, (error)=>{
|
||||
content.msgtype = 'm.file';
|
||||
def.resolve();
|
||||
});
|
||||
@@ -331,7 +325,7 @@ class ContentMessages {
|
||||
this.inprogress.push(upload);
|
||||
dis.dispatch({action: 'upload_started'});
|
||||
|
||||
var error;
|
||||
let error;
|
||||
|
||||
function onProgress(ev) {
|
||||
upload.total = ev.total;
|
||||
@@ -355,11 +349,11 @@ class ContentMessages {
|
||||
}, function(err) {
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
var desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
||||
if (err.http_status == 413) {
|
||||
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
||||
}
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
title: _t('Upload Failed'),
|
||||
description: desc,
|
||||
@@ -367,8 +361,8 @@ class ContentMessages {
|
||||
}
|
||||
}).finally(() => {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
for (var i = 0; i < this.inprogress.length; ++i) {
|
||||
var k = inprogressKeys[i];
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === upload.promise) {
|
||||
this.inprogress.splice(k, 1);
|
||||
break;
|
||||
@@ -376,8 +370,7 @@ class ContentMessages {
|
||||
}
|
||||
if (error) {
|
||||
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||
}
|
||||
});
|
||||
@@ -389,9 +382,9 @@ class ContentMessages {
|
||||
|
||||
cancelUpload(promise) {
|
||||
const inprogressKeys = Object.keys(this.inprogress);
|
||||
var upload;
|
||||
for (var i = 0; i < this.inprogress.length; ++i) {
|
||||
var k = inprogressKeys[i];
|
||||
let upload;
|
||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||
const k = inprogressKeys[i];
|
||||
if (this.inprogress[k].promise === promise) {
|
||||
upload = this.inprogress[k];
|
||||
break;
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return this.formatTime(date);
|
||||
return this.formatTime(date, showTwelveHour);
|
||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
return _t('%(weekDayName)s %(time)s', {
|
||||
@@ -78,7 +78,7 @@ module.exports = {
|
||||
weekDayName: days[date.getDay()],
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
time: this.formatTime(date),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
});
|
||||
}
|
||||
return this.formatFullDate(date, showTwelveHour);
|
||||
@@ -92,13 +92,13 @@ module.exports = {
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
time: showTwelveHour ? twelveHourTime(date) : this.formatTime(date),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
});
|
||||
},
|
||||
|
||||
formatTime: function(date, showTwelveHour=false) {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date);
|
||||
return twelveHourTime(date);
|
||||
}
|
||||
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||
},
|
||||
|
||||
156
src/GroupAddressPicker.js
Normal file
156
src/GroupAddressPicker.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Modal from './Modal';
|
||||
import sdk from './';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import { _t } from './languageHandler';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import GroupStoreCache from './stores/GroupStoreCache';
|
||||
|
||||
export function showGroupInviteDialog(groupId) {
|
||||
const description = <div>
|
||||
<div>{ _t("Who would you like to add to this community?") }</div>
|
||||
<div className="warning">
|
||||
{ _t(
|
||||
"Warning: any person you add to a community will be publicly "+
|
||||
"visible to anyone who knows the community ID",
|
||||
) }
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
|
||||
title: _t("Invite new community members"),
|
||||
description: description,
|
||||
placeholder: _t("Name or matrix ID"),
|
||||
button: _t("Invite to Community"),
|
||||
validAddressTypes: ['mx-user-id'],
|
||||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
|
||||
_onGroupInviteFinished(groupId, addrs);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function showGroupAddRoomDialog(groupId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let addRoomsPublicly = false;
|
||||
const onCheckboxClicked = (e) => {
|
||||
addRoomsPublicly = e.target.checked;
|
||||
};
|
||||
const description = <div>
|
||||
<div>{ _t("Which rooms would you like to add to this community?") }</div>
|
||||
</div>;
|
||||
|
||||
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
|
||||
<input type="checkbox" onClick={onCheckboxClicked} />
|
||||
<div>
|
||||
{ _t("Show these rooms to non-members on the community page and room list?") }
|
||||
</div>
|
||||
</label>;
|
||||
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
|
||||
title: _t("Add rooms to the community"),
|
||||
description: description,
|
||||
extraNode: checkboxContainer,
|
||||
placeholder: _t("Room name or alias"),
|
||||
button: _t("Add to community"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
|
||||
_onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _onGroupInviteFinished(groupId, addrs) {
|
||||
const multiInviter = new MultiInviter(groupId);
|
||||
|
||||
const addrTexts = addrs.map((addr) => addr.address);
|
||||
|
||||
multiInviter.invite(addrTexts).then((completionStates) => {
|
||||
// Show user any errors
|
||||
const errorList = [];
|
||||
for (const addr of Object.keys(completionStates)) {
|
||||
if (addrs[addr] === "error") {
|
||||
errorList.push(addr);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorList.length > 0) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
|
||||
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
|
||||
title: _t("Failed to invite users to community"),
|
||||
description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||
const matrixClient = MatrixClientPeg.get();
|
||||
const groupStore = GroupStoreCache.getGroupStore(groupId);
|
||||
const errorList = [];
|
||||
return Promise.all(addrs.map((addr) => {
|
||||
return groupStore
|
||||
.addRoomToGroup(addr.address, addRoomsPublicly)
|
||||
.catch(() => { errorList.push(addr.address); })
|
||||
.then(() => {
|
||||
const roomId = addr.address;
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
// Can the user change related groups?
|
||||
if (!room || !room.currentState.mayClientSendStateEvent("m.room.related_groups", matrixClient)) {
|
||||
return;
|
||||
}
|
||||
// Get the related groups
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
const groups = relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [];
|
||||
|
||||
// Add this group as related
|
||||
if (!groups.includes(groupId)) {
|
||||
groups.push(groupId);
|
||||
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, '');
|
||||
}
|
||||
}).reflect();
|
||||
})).then(() => {
|
||||
if (errorList.length === 0) {
|
||||
return;
|
||||
}
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to add the following room to the group',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to %(groupId)s:",
|
||||
{groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
});
|
||||
}
|
||||
129
src/HtmlUtils.js
129
src/HtmlUtils.js
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,10 +17,10 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var sanitizeHtml = require('sanitize-html');
|
||||
var highlight = require('highlight.js');
|
||||
var linkifyMatrix = require('./linkify-matrix');
|
||||
const React = require('react');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
const highlight = require('highlight.js');
|
||||
const linkifyMatrix = require('./linkify-matrix');
|
||||
import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
@@ -31,13 +32,33 @@ emojione.imagePathPNG = 'emojione/png/';
|
||||
// Use SVGs for emojis
|
||||
emojione.imageType = 'svg';
|
||||
|
||||
// Anything outside the basic multilingual plane will be a surrogate pair
|
||||
const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||
// And there a bunch more symbol characters that emojione has within the
|
||||
// BMP, so this includes the ranges from 'letterlike symbols' to
|
||||
// 'miscellaneous symbols and arrows' which should catch all of them
|
||||
// (with plenty of false positives, but that's OK)
|
||||
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||
|
||||
// And this is emojione's complete regex
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojione's so will give false
|
||||
* positives, but useful for fast-path testing strings to see if they
|
||||
* need emojification.
|
||||
* unicodeToImage uses this function.
|
||||
*/
|
||||
export function containsEmoji(str) {
|
||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||
}
|
||||
|
||||
/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js
|
||||
* because we want to include emoji shortnames in title text
|
||||
*/
|
||||
export function unicodeToImage(str) {
|
||||
function unicodeToImage(str) {
|
||||
let replaceWith, unicode, alt, short, fname;
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
|
||||
@@ -45,8 +66,7 @@ export function unicodeToImage(str) {
|
||||
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
@@ -127,7 +147,7 @@ export function processHtmlForSending(html: string): string {
|
||||
* of that HTML.
|
||||
*/
|
||||
export function sanitizedHtmlNode(insaneHtml) {
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
@@ -136,7 +156,7 @@ const sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'del', // for markdown
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'sup', 'sub',
|
||||
'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'span', 'img',
|
||||
],
|
||||
@@ -152,7 +172,7 @@ const sanitizeHtmlParams = {
|
||||
// Lots of these won't come up by default because we don't allow them
|
||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
|
||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
|
||||
|
||||
allowProtocolRelative: false,
|
||||
|
||||
@@ -162,21 +182,19 @@ const sanitizeHtmlParams = {
|
||||
if (attribs.href) {
|
||||
attribs.target = '_blank'; // by default
|
||||
|
||||
var m;
|
||||
let m;
|
||||
// FIXME: horrible duplication with linkify-matrix
|
||||
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
|
||||
if (m) {
|
||||
attribs.href = m[1];
|
||||
delete attribs.target;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||
if (m) {
|
||||
var entity = m[1];
|
||||
const entity = m[1];
|
||||
if (entity[0] === '@') {
|
||||
attribs.href = '#/user/' + entity;
|
||||
}
|
||||
else if (entity[0] === '#' || entity[0] === '!') {
|
||||
} else if (entity[0] === '#' || entity[0] === '!') {
|
||||
attribs.href = '#/room/' + entity;
|
||||
}
|
||||
delete attribs.target;
|
||||
@@ -184,13 +202,13 @@ const sanitizeHtmlParams = {
|
||||
}
|
||||
}
|
||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
return { tagName: tagName, attribs : attribs };
|
||||
return { tagName: tagName, attribs: attribs };
|
||||
},
|
||||
'img': function(tagName, attribs) {
|
||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||
// we don't want to allow images with `https?` `src`s.
|
||||
if (!attribs.src.startsWith('mxc://')) {
|
||||
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
|
||||
return { tagName, attribs: {}};
|
||||
}
|
||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
@@ -203,7 +221,7 @@ const sanitizeHtmlParams = {
|
||||
'code': function(tagName, attribs) {
|
||||
if (typeof attribs.class !== 'undefined') {
|
||||
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||
let classes = attribs.class.split(/\s+/).filter(function(cl) {
|
||||
const classes = attribs.class.split(/\s+/).filter(function(cl) {
|
||||
return cl.startsWith('language-');
|
||||
});
|
||||
attribs.class = classes.join(' ');
|
||||
@@ -266,11 +284,11 @@ class BaseHighlighter {
|
||||
* TextHighlighter).
|
||||
*/
|
||||
applyHighlights(safeSnippet, safeHighlights) {
|
||||
var lastOffset = 0;
|
||||
var offset;
|
||||
var nodes = [];
|
||||
let lastOffset = 0;
|
||||
let offset;
|
||||
let nodes = [];
|
||||
|
||||
var safeHighlight = safeHighlights[0];
|
||||
const safeHighlight = safeHighlights[0];
|
||||
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||
// handle preamble
|
||||
if (offset > lastOffset) {
|
||||
@@ -280,7 +298,7 @@ class BaseHighlighter {
|
||||
|
||||
// do highlight. use the original string rather than safeHighlight
|
||||
// to preserve the original casing.
|
||||
var endOffset = offset + safeHighlight.length;
|
||||
const endOffset = offset + safeHighlight.length;
|
||||
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
|
||||
|
||||
lastOffset = endOffset;
|
||||
@@ -298,8 +316,7 @@ class BaseHighlighter {
|
||||
if (safeHighlights[1]) {
|
||||
// recurse into this range to check for the next set of highlight matches
|
||||
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// no more highlights to be found, just return the unhighlighted string
|
||||
return [this._processSnippet(safeSnippet, false)];
|
||||
}
|
||||
@@ -320,7 +337,7 @@ class HtmlHighlighter extends BaseHighlighter {
|
||||
return snippet;
|
||||
}
|
||||
|
||||
var span = "<span class=\""+this.highlightClass+"\">"
|
||||
let span = "<span class=\""+this.highlightClass+"\">"
|
||||
+ snippet + "</span>";
|
||||
|
||||
if (this.highlightLink) {
|
||||
@@ -345,15 +362,15 @@ class TextHighlighter extends BaseHighlighter {
|
||||
* returns a React node
|
||||
*/
|
||||
_processSnippet(snippet, highlight) {
|
||||
var key = this._key++;
|
||||
const key = this._key++;
|
||||
|
||||
var node =
|
||||
<span key={key} className={highlight ? this.highlightClass : null }>
|
||||
let node =
|
||||
<span key={key} className={highlight ? this.highlightClass : null}>
|
||||
{ snippet }
|
||||
</span>;
|
||||
|
||||
if (highlight && this.highlightLink) {
|
||||
node = <a key={key} href={this.highlightLink}>{node}</a>;
|
||||
node = <a key={key} href={this.highlightLink}>{ node }</a>;
|
||||
}
|
||||
|
||||
return node;
|
||||
@@ -368,22 +385,23 @@ class TextHighlighter extends BaseHighlighter {
|
||||
* highlights: optional list of words to highlight, ordered by longest word first
|
||||
*
|
||||
* opts.highlightLink: optional href to add to highlighted words
|
||||
* opts.disableBigEmoji: optional argument to disable the big emoji class.
|
||||
*/
|
||||
export function bodyToHtml(content, highlights, opts) {
|
||||
opts = opts || {};
|
||||
export function bodyToHtml(content, highlights, opts={}) {
|
||||
const isHtml = (content.format === "org.matrix.custom.html");
|
||||
const body = isHtml ? content.formatted_body : escape(content.body);
|
||||
|
||||
var isHtml = (content.format === "org.matrix.custom.html");
|
||||
let body = isHtml ? content.formatted_body : escape(content.body);
|
||||
let bodyHasEmoji = false;
|
||||
|
||||
var safeBody;
|
||||
let safeBody;
|
||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||
try {
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
var safeHighlights = highlights.map(function(highlight) {
|
||||
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||
const safeHighlights = highlights.map(function(highlight) {
|
||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||
});
|
||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||
@@ -392,17 +410,19 @@ export function bodyToHtml(content, highlights, opts) {
|
||||
};
|
||||
}
|
||||
safeBody = sanitizeHtml(body, sanitizeHtmlParams);
|
||||
safeBody = unicodeToImage(safeBody);
|
||||
safeBody = addCodeCopyButton(safeBody);
|
||||
}
|
||||
finally {
|
||||
bodyHasEmoji = containsEmoji(body);
|
||||
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
|
||||
} finally {
|
||||
delete sanitizeHtmlParams.textFilter;
|
||||
}
|
||||
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||
let match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
let emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
let emojiBody = false;
|
||||
if (!opts.disableBigEmoji && bodyHasEmoji) {
|
||||
EMOJI_REGEX.lastIndex = 0;
|
||||
const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
|
||||
const match = EMOJI_REGEX.exec(contentBodyTrimmed);
|
||||
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
|
||||
}
|
||||
|
||||
const className = classNames({
|
||||
'mx_EventTile_body': true,
|
||||
@@ -412,23 +432,6 @@ export function bodyToHtml(content, highlights, opts) {
|
||||
return <span className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" />;
|
||||
}
|
||||
|
||||
function addCodeCopyButton(safeBody) {
|
||||
// Adds 'copy' buttons to pre blocks
|
||||
// Note that this only manipulates the markup to add the buttons:
|
||||
// we need to add the event handlers once the nodes are in the DOM
|
||||
// since we can't save functions in the markup.
|
||||
// This is done in TextualBody
|
||||
const el = document.createElement("div");
|
||||
el.innerHTML = safeBody;
|
||||
const codeBlocks = Array.from(el.getElementsByTagName("pre"));
|
||||
codeBlocks.forEach(p => {
|
||||
const button = document.createElement("span");
|
||||
button.className = "mx_EventTile_copyButton";
|
||||
p.appendChild(button);
|
||||
});
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
export function emojifyText(text) {
|
||||
return {
|
||||
__html: unicodeToImage(escape(text)),
|
||||
|
||||
@@ -42,13 +42,12 @@ module.exports = {
|
||||
// no scaling needs to be applied
|
||||
return fullHeight;
|
||||
}
|
||||
var widthMulti = thumbWidth / fullWidth;
|
||||
var heightMulti = thumbHeight / fullHeight;
|
||||
const widthMulti = thumbWidth / fullWidth;
|
||||
const heightMulti = thumbHeight / fullHeight;
|
||||
if (widthMulti < heightMulti) {
|
||||
// width is the dominant dimension so scaling will be fixed on that
|
||||
return Math.floor(widthMulti * fullHeight);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// height is the dominant dimension so scaling will be fixed on that
|
||||
return Math.floor(heightMulti * fullHeight);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
/* a selection of key codes, as used in KeyboardEvent.keyCode */
|
||||
module.exports = {
|
||||
export const KeyCode = {
|
||||
BACKSPACE: 8,
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
@@ -58,3 +59,12 @@ module.exports = {
|
||||
KEY_Y: 89,
|
||||
KEY_Z: 90,
|
||||
};
|
||||
|
||||
export function isOnlyCtrlOrCmdKeyEvent(ev) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
if (isMac) {
|
||||
return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||
} else {
|
||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
}
|
||||
@@ -389,6 +389,8 @@ function _persistCredentialsToLocalStorage(credentials) {
|
||||
* Logs the current session out and transitions to the logged-out state
|
||||
*/
|
||||
export function logout() {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
// logout doesn't work for guest sessions
|
||||
// Also we sometimes want to re-log in a guest session
|
||||
@@ -436,6 +438,10 @@ function startMatrixClient() {
|
||||
DMRoomMap.makeShared().start();
|
||||
|
||||
MatrixClientPeg.start();
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
dis.dispatch({action: 'client_started'});
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
96
src/Login.js
96
src/Login.js
@@ -59,8 +59,8 @@ export default class Login {
|
||||
}
|
||||
|
||||
getFlows() {
|
||||
var self = this;
|
||||
var client = this._createTemporaryClient();
|
||||
const self = this;
|
||||
const client = this._createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
@@ -77,12 +77,12 @@ export default class Login {
|
||||
getCurrentFlowStep() {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login so we can ignore it.
|
||||
var flowStep = this._flows[this._currentFlowIndex];
|
||||
const flowStep = this._flows[this._currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginAsGuest() {
|
||||
var client = this._createTemporaryClient();
|
||||
const client = this._createTemporaryClient();
|
||||
return client.registerGuest({
|
||||
body: {
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
@@ -94,7 +94,7 @@ export default class Login {
|
||||
accessToken: creds.access_token,
|
||||
homeserverUrl: this._hsUrl,
|
||||
identityServerUrl: this._isUrl,
|
||||
guest: true
|
||||
guest: true,
|
||||
};
|
||||
}, (error) => {
|
||||
throw error;
|
||||
@@ -143,36 +143,84 @@ export default class Login {
|
||||
Object.assign(loginParams, legacyParams);
|
||||
|
||||
const client = this._createTemporaryClient();
|
||||
|
||||
const tryFallbackHs = (originalError) => {
|
||||
const fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}).catch((fallback_error) => {
|
||||
console.log("fallback HS login failed", fallback_error);
|
||||
// throw the original error
|
||||
throw originalError;
|
||||
});
|
||||
};
|
||||
const tryLowercaseUsername = (originalError) => {
|
||||
const loginParamsLowercase = Object.assign({}, loginParams, {
|
||||
user: username.toLowerCase(),
|
||||
identifier: {
|
||||
user: username.toLowerCase(),
|
||||
},
|
||||
});
|
||||
return client.login('m.login.password', loginParamsLowercase).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}).catch((fallback_error) => {
|
||||
console.log("Lowercase username login failed", fallback_error);
|
||||
// throw the original error
|
||||
throw originalError;
|
||||
});
|
||||
};
|
||||
|
||||
let originalLoginError = null;
|
||||
return client.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._hsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
}, function(error) {
|
||||
}).catch((error) => {
|
||||
originalLoginError = error;
|
||||
if (error.httpStatus === 403) {
|
||||
if (self._fallbackHsUrl) {
|
||||
var fbClient = Matrix.createClient({
|
||||
baseUrl: self._fallbackHsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
|
||||
return fbClient.login('m.login.password', loginParams).then(function(data) {
|
||||
return Promise.resolve({
|
||||
homeserverUrl: self._fallbackHsUrl,
|
||||
identityServerUrl: self._isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token
|
||||
});
|
||||
}, function(fallback_error) {
|
||||
// throw the original error
|
||||
throw error;
|
||||
});
|
||||
return tryFallbackHs(originalLoginError);
|
||||
}
|
||||
}
|
||||
throw originalLoginError;
|
||||
}).catch((error) => {
|
||||
// We apparently squash case at login serverside these days:
|
||||
// https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
|
||||
// so this wasn't needed after all. Keeping the code around in case the
|
||||
// the situation changes...
|
||||
|
||||
/*
|
||||
if (
|
||||
error.httpStatus === 403 &&
|
||||
loginParams.identifier.type === 'm.id.user' &&
|
||||
username.search(/[A-Z]/) > -1
|
||||
) {
|
||||
return tryLowercaseUsername(originalLoginError);
|
||||
}
|
||||
*/
|
||||
throw originalLoginError;
|
||||
}).catch((error) => {
|
||||
console.log("Login failed", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import commonmark from 'commonmark';
|
||||
import escape from 'lodash/escape';
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['del', 'u'];
|
||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||
|
||||
// These types of node are definitely text
|
||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||
@@ -48,7 +48,7 @@ function html_if_tag_allowed(node) {
|
||||
* or false if it is only a single line.
|
||||
*/
|
||||
function is_multi_line(node) {
|
||||
var par = node;
|
||||
let par = node;
|
||||
while (par.parent) {
|
||||
par = par.parent;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export default class Markdown {
|
||||
if (isMultiLine) this.cr();
|
||||
html_if_tag_allowed.call(this, node);
|
||||
if (isMultiLine) this.cr();
|
||||
}
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ export default class Markdown {
|
||||
renderer.html_block = function(node) {
|
||||
this.lit(node.literal);
|
||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
||||
}
|
||||
};
|
||||
|
||||
return renderer.render(this.parsed);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +22,8 @@ import utils from 'matrix-js-sdk/lib/utils';
|
||||
import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline';
|
||||
import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
|
||||
import createMatrixClient from './utils/createMatrixClient';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import MatrixActionCreators from './actions/MatrixActionCreators';
|
||||
|
||||
interface MatrixClientCreds {
|
||||
homeserverUrl: string,
|
||||
@@ -67,6 +70,8 @@ class MatrixClientPeg {
|
||||
|
||||
unset() {
|
||||
this.matrixClient = null;
|
||||
|
||||
MatrixActionCreators.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +89,7 @@ class MatrixClientPeg {
|
||||
if (this.matrixClient.initCrypto) {
|
||||
await this.matrixClient.initCrypto();
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// this can happen for a number of reasons, the most likely being
|
||||
// that the olm library was missing. It's not fatal.
|
||||
console.warn("Unable to initialise e2e: " + e);
|
||||
@@ -93,12 +98,13 @@ class MatrixClientPeg {
|
||||
const opts = utils.deepCopy(this.opts);
|
||||
// the react sdk doesn't work without this, so don't allow
|
||||
opts.pendingEventOrdering = "detached";
|
||||
opts.disablePresence = true; // we do this manually
|
||||
|
||||
try {
|
||||
let promise = this.matrixClient.store.startup();
|
||||
const promise = this.matrixClient.store.startup();
|
||||
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
|
||||
await promise;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
// log any errors when starting up the database (if one exists)
|
||||
console.error(`Error starting matrixclient store: ${err}`);
|
||||
}
|
||||
@@ -106,6 +112,9 @@ class MatrixClientPeg {
|
||||
// regardless of errors, start the client. If we did error out, we'll
|
||||
// just end up doing a full initial /sync.
|
||||
|
||||
// Connect the matrix client to the dispatcher
|
||||
MatrixActionCreators.start(this.matrixClient);
|
||||
|
||||
console.log(`MatrixClientPeg: really starting MatrixClient`);
|
||||
this.get().startClient(opts);
|
||||
console.log(`MatrixClientPeg: MatrixClient started`);
|
||||
@@ -136,13 +145,14 @@ class MatrixClientPeg {
|
||||
}
|
||||
|
||||
_createClient(creds: MatrixClientCreds) {
|
||||
var opts = {
|
||||
const opts = {
|
||||
baseUrl: creds.homeserverUrl,
|
||||
idBaseUrl: creds.identityServerUrl,
|
||||
accessToken: creds.accessToken,
|
||||
userId: creds.userId,
|
||||
deviceId: creds.deviceId,
|
||||
timelineSupport: true,
|
||||
forceTURN: SettingsStore.getValue('webRtcForceTURN', false),
|
||||
};
|
||||
|
||||
this.matrixClient = createMatrixClient(opts, this.indexedDbWorkerScript);
|
||||
@@ -153,8 +163,8 @@ class MatrixClientPeg {
|
||||
|
||||
this.matrixClient.setGuest(Boolean(creds.guest));
|
||||
|
||||
var notifTimelineSet = new EventTimelineSet(null, {
|
||||
timelineSupport: true
|
||||
const notifTimelineSet = new EventTimelineSet(null, {
|
||||
timelineSupport: true,
|
||||
});
|
||||
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
|
||||
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);
|
||||
|
||||
22
src/Modal.js
22
src/Modal.js
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import Analytics from './Analytics';
|
||||
import sdk from './index';
|
||||
|
||||
@@ -137,15 +137,15 @@ class ModalManager {
|
||||
* @param {String} className CSS class to apply to the modal wrapper
|
||||
*/
|
||||
createDialogAsync(loader, props, className) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
const modal = {};
|
||||
|
||||
// never call this from onFinished() otherwise it will loop
|
||||
//
|
||||
// nb explicit function() rather than arrow function, to get `arguments`
|
||||
var closeDialog = function() {
|
||||
const closeDialog = function() {
|
||||
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||
var i = self._modals.indexOf(modal);
|
||||
const i = self._modals.indexOf(modal);
|
||||
if (i >= 0) {
|
||||
self._modals.splice(i, 1);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ class ModalManager {
|
||||
// property set here so you can't close the dialog from a button click!
|
||||
modal.elem = (
|
||||
<AsyncWrapper key={modalCount} loader={loader} {...props}
|
||||
onFinished={closeDialog}/>
|
||||
onFinished={closeDialog} />
|
||||
);
|
||||
modal.onFinished = props ? props.onFinished : null;
|
||||
modal.className = className;
|
||||
@@ -191,13 +191,13 @@ class ModalManager {
|
||||
return;
|
||||
}
|
||||
|
||||
var modal = this._modals[0];
|
||||
var dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }>
|
||||
const modal = this._modals[0];
|
||||
const dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||
<div className="mx_Dialog">
|
||||
{modal.elem}
|
||||
{ modal.elem }
|
||||
</div>
|
||||
<div className="mx_Dialog_background" onClick={ this.closeAll }></div>
|
||||
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +25,7 @@ import dis from './dispatcher';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
@@ -79,10 +81,11 @@ const Notifier = {
|
||||
if (ev.getContent().body) msg = ev.getContent().body;
|
||||
}
|
||||
|
||||
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
|
||||
ev.sender, 40, 40, 'crop'
|
||||
) : null;
|
||||
if (!this.isBodyEnabled()) {
|
||||
msg = '';
|
||||
}
|
||||
|
||||
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
|
||||
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
|
||||
|
||||
// if displayNotification returns non-null, the platform supports
|
||||
@@ -96,17 +99,16 @@ const Notifier = {
|
||||
_playAudioNotification: function(ev, room) {
|
||||
const e = document.getElementById("messageAudio");
|
||||
if (e) {
|
||||
e.load();
|
||||
e.play();
|
||||
}
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
||||
this.boundOnEvent = this.onEvent.bind(this);
|
||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
||||
this.boundOnEventDecrypted = this.onEventDecrypted.bind(this);
|
||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
||||
MatrixClientPeg.get().on('event', this.boundOnEvent);
|
||||
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
||||
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||
@@ -116,7 +118,7 @@ const Notifier = {
|
||||
|
||||
stop: function() {
|
||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener('Event', this.boundOnEvent);
|
||||
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
||||
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||
@@ -137,10 +139,8 @@ const Notifier = {
|
||||
|
||||
// make sure that we persist the current setting audio_enabled setting
|
||||
// before changing anything
|
||||
if (global.localStorage) {
|
||||
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
|
||||
this.setAudioEnabled(this.isEnabled());
|
||||
}
|
||||
if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
|
||||
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
@@ -148,6 +148,7 @@ const Notifier = {
|
||||
plaf.requestNotificationPermission().done((result) => {
|
||||
if (result !== 'granted') {
|
||||
// The permission request was dismissed or denied
|
||||
// TODO: Support alternative branding in messaging
|
||||
const description = result === 'denied'
|
||||
? _t('Riot does not have permission to send you notifications - please check your browser settings')
|
||||
: _t('Riot was not given permission to send notifications - please try again');
|
||||
@@ -159,10 +160,6 @@ const Notifier = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (global.localStorage) {
|
||||
global.localStorage.setItem('notifications_enabled', 'true');
|
||||
}
|
||||
|
||||
if (callback) callback();
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
@@ -173,8 +170,6 @@ const Notifier = {
|
||||
// disabled again in the future, we will show the banner again.
|
||||
this.setToolbarHidden(false);
|
||||
} else {
|
||||
if (!global.localStorage) return;
|
||||
global.localStorage.setItem('notifications_enabled', 'false');
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: false,
|
||||
@@ -183,31 +178,24 @@ const Notifier = {
|
||||
},
|
||||
|
||||
isEnabled: function() {
|
||||
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
|
||||
},
|
||||
|
||||
isPossible: function() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (!plaf) return false;
|
||||
if (!plaf.supportsNotifications()) return false;
|
||||
if (!plaf.maySendNotifications()) return false;
|
||||
|
||||
if (!global.localStorage) return true;
|
||||
|
||||
const enabled = global.localStorage.getItem('notifications_enabled');
|
||||
if (enabled === null) return true;
|
||||
return enabled === 'true';
|
||||
return true; // possible, but not necessarily enabled
|
||||
},
|
||||
|
||||
setAudioEnabled: function(enable) {
|
||||
if (!global.localStorage) return;
|
||||
global.localStorage.setItem('audio_notifications_enabled',
|
||||
enable ? 'true' : 'false');
|
||||
isBodyEnabled: function() {
|
||||
return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
|
||||
},
|
||||
|
||||
isAudioEnabled: function(enable) {
|
||||
if (!global.localStorage) return true;
|
||||
const enabled = global.localStorage.getItem(
|
||||
'audio_notifications_enabled');
|
||||
// default to true if the popups are enabled
|
||||
if (enabled === null) return this.isEnabled();
|
||||
return enabled === 'true';
|
||||
isAudioEnabled: function() {
|
||||
return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
|
||||
},
|
||||
|
||||
setToolbarHidden: function(hidden, persistent = true) {
|
||||
@@ -224,16 +212,14 @@ const Notifier = {
|
||||
|
||||
// update the info to localStorage for persistent settings
|
||||
if (persistent && global.localStorage) {
|
||||
global.localStorage.setItem('notifications_hidden', hidden);
|
||||
global.localStorage.setItem("notifications_hidden", hidden);
|
||||
}
|
||||
},
|
||||
|
||||
isToolbarHidden: function() {
|
||||
// Check localStorage for any such meta data
|
||||
if (global.localStorage) {
|
||||
if (global.localStorage.getItem('notifications_hidden') === 'true') {
|
||||
return true;
|
||||
}
|
||||
return global.localStorage.getItem("notifications_hidden") === "true";
|
||||
}
|
||||
|
||||
return this.toolbarHidden;
|
||||
@@ -247,12 +233,9 @@ const Notifier = {
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (toStartOfTimeline) return;
|
||||
if (!room) return;
|
||||
onEvent: function(ev) {
|
||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||
if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
|
||||
// If it's an encrypted event and the type is still 'm.room.encrypted',
|
||||
// it hasn't yet been decrypted, so wait until it is.
|
||||
@@ -306,7 +289,7 @@ const Notifier = {
|
||||
this._playAudioNotification(ev, room);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!global.mxNotifier) {
|
||||
|
||||
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var dis = require("./dispatcher");
|
||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||
const dis = require("./dispatcher");
|
||||
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||
const PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||
|
||||
class Presence {
|
||||
|
||||
@@ -56,13 +56,27 @@ class Presence {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status message.
|
||||
* @returns {String} the status message, may be null
|
||||
*/
|
||||
getStatusMessage() {
|
||||
return this.statusMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||
* @param {String} statusMessage an optional status message for the presence
|
||||
* @param {boolean} maintain true to have this status maintained by this tracker
|
||||
*/
|
||||
setState(newState) {
|
||||
if (newState === this.state) {
|
||||
setState(newState, statusMessage=null, maintain=false) {
|
||||
if (this.maintain) {
|
||||
// Don't update presence if we're maintaining a particular status
|
||||
return;
|
||||
}
|
||||
if (newState === this.state && statusMessage === this.statusMessage) {
|
||||
return;
|
||||
}
|
||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||
@@ -71,22 +85,38 @@ class Presence {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
var old_state = this.state;
|
||||
const old_state = this.state;
|
||||
const old_message = this.statusMessage;
|
||||
this.state = newState;
|
||||
this.statusMessage = statusMessage;
|
||||
this.maintain = maintain;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return; // don't try to set presence when a guest; it won't work.
|
||||
}
|
||||
|
||||
var self = this;
|
||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||
const updateContent = {
|
||||
presence: this.state,
|
||||
status_msg: this.statusMessage ? this.statusMessage : '',
|
||||
};
|
||||
|
||||
const self = this;
|
||||
MatrixClientPeg.get().setPresence(updateContent).done(function() {
|
||||
console.log("Presence: %s", newState);
|
||||
|
||||
// We have to dispatch because the js-sdk is unreliable at telling us about our own presence
|
||||
dis.dispatch({action: "self_presence_updated", statusInfo: updateContent});
|
||||
}, function(err) {
|
||||
console.error("Failed to set presence: %s", err);
|
||||
self.state = old_state;
|
||||
self.statusMessage = old_message;
|
||||
});
|
||||
}
|
||||
|
||||
stopMaintainingStatus() {
|
||||
this.maintain = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||
* @private
|
||||
@@ -95,7 +125,8 @@ class Presence {
|
||||
this.setState("unavailable");
|
||||
}
|
||||
|
||||
_onUserActivity() {
|
||||
_onUserActivity(payload) {
|
||||
if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
|
||||
this._resetTimer();
|
||||
}
|
||||
|
||||
@@ -104,7 +135,7 @@ class Presence {
|
||||
* @private
|
||||
*/
|
||||
_resetTimer() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
this.setState("online");
|
||||
// Re-arm the timer
|
||||
clearTimeout(this.timer);
|
||||
|
||||
@@ -44,13 +44,6 @@ module.exports = {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/riot-web/issues/3148
|
||||
console.log('Resend got send failure: ' + err.name + '('+err+')');
|
||||
if (err.name === "UnknownDeviceError") {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: room,
|
||||
});
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'message_send_failed',
|
||||
@@ -60,9 +53,5 @@ module.exports = {
|
||||
},
|
||||
removeFromQueue: function(event) {
|
||||
MatrixClientPeg.get().cancelPendingEvent(event);
|
||||
dis.dispatch({
|
||||
action: 'message_send_cancelled',
|
||||
event: event,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,9 +44,9 @@ export const contentStateToHTML = (contentState: ContentState) => {
|
||||
return stateToHTML(contentState, {
|
||||
inlineStyles: {
|
||||
UNDERLINE: {
|
||||
element: 'u'
|
||||
}
|
||||
}
|
||||
element: 'u',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ function unicodeToEmojiUri(str) {
|
||||
let replaceWith, unicode, alt;
|
||||
if ((!emojione.unicodeAlt) || (emojione.sprites)) {
|
||||
// if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
|
||||
let mappedUnicode = emojione.mapUnicodeToShort();
|
||||
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||
}
|
||||
|
||||
str = str.replace(emojione.regUnicode, function(unicodeChar) {
|
||||
@@ -67,8 +67,14 @@ function unicodeToEmojiUri(str) {
|
||||
// if the unicodeChar doesnt exist just return the entire match
|
||||
return unicodeChar;
|
||||
} else {
|
||||
// Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
|
||||
if (unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
|
||||
unicodeChar = unicodeChar[0];
|
||||
}
|
||||
|
||||
// get the unicode codepoint from the actual char
|
||||
unicode = emojione.jsEscapeMap[unicodeChar];
|
||||
|
||||
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
|
||||
}
|
||||
});
|
||||
@@ -90,14 +96,14 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||
let emojiDecorator = {
|
||||
const emojiDecorator = {
|
||||
strategy: (contentState, contentBlock, callback) => {
|
||||
findWithRegex(EMOJI_REGEX, contentBlock, callback);
|
||||
},
|
||||
component: (props) => {
|
||||
let uri = unicodeToEmojiUri(props.children[0].props.text);
|
||||
let shortname = emojione.toShort(props.children[0].props.text);
|
||||
let style = {
|
||||
const uri = unicodeToEmojiUri(props.children[0].props.text);
|
||||
const shortname = emojione.toShort(props.children[0].props.text);
|
||||
const style = {
|
||||
display: 'inline-block',
|
||||
width: '1em',
|
||||
maxHeight: '1em',
|
||||
@@ -106,7 +112,7 @@ let emojiDecorator = {
|
||||
backgroundPosition: 'center center',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{props.children}</span></span>);
|
||||
return (<span title={shortname} style={style}><span style={{opacity: 0}}>{ props.children }</span></span>);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -118,16 +124,16 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||
}
|
||||
|
||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||
const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||
(style) => ({
|
||||
strategy: (contentState, contentBlock, callback) => {
|
||||
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
||||
},
|
||||
component: (props) => (
|
||||
<span className={"mx_MarkdownElement mx_Markdown_" + style}>
|
||||
{props.children}
|
||||
{ props.children }
|
||||
</span>
|
||||
)
|
||||
),
|
||||
}));
|
||||
|
||||
markdownDecorators.push({
|
||||
@@ -136,9 +142,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||
},
|
||||
component: (props) => (
|
||||
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
|
||||
{props.children}
|
||||
{ props.children }
|
||||
</a>
|
||||
)
|
||||
),
|
||||
});
|
||||
// markdownDecorators.push(emojiDecorator);
|
||||
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||
@@ -161,7 +167,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||
for (let currentKey = startKey;
|
||||
currentKey && currentKey !== endKey;
|
||||
currentKey = contentState.getKeyAfter(currentKey)) {
|
||||
let blockText = getText(currentKey);
|
||||
const blockText = getText(currentKey);
|
||||
text += blockText.substring(startOffset, blockText.length);
|
||||
|
||||
// from now on, we'll take whole blocks
|
||||
@@ -182,7 +188,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
||||
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||
let offset = 0, start = 0, end = 0;
|
||||
for (let block of contentBlocks) {
|
||||
for (const block of contentBlocks) {
|
||||
if (selectionState.getStartKey() === block.getKey()) {
|
||||
start = offset + selectionState.getStartOffset();
|
||||
}
|
||||
@@ -259,7 +265,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
|
||||
.set('focusOffset', end);
|
||||
const emojiText = plainText.substring(start, end);
|
||||
newContentState = newContentState.createEntity(
|
||||
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText }
|
||||
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
|
||||
);
|
||||
const entityKey = newContentState.getLastCreatedEntityKey();
|
||||
newContentState = Modifier.replaceText(
|
||||
|
||||
11
src/Roles.js
11
src/Roles.js
@@ -15,19 +15,20 @@ limitations under the License.
|
||||
*/
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
export function levelRoleMap() {
|
||||
export function levelRoleMap(usersDefault) {
|
||||
return {
|
||||
undefined: _t('Default'),
|
||||
0: _t('User'),
|
||||
0: _t('Restricted'),
|
||||
[usersDefault]: _t('Default'),
|
||||
50: _t('Moderator'),
|
||||
100: _t('Admin'),
|
||||
};
|
||||
}
|
||||
|
||||
export function textualPowerLevel(level, userDefault) {
|
||||
const LEVEL_ROLE_MAP = this.levelRoleMap();
|
||||
export function textualPowerLevel(level, usersDefault) {
|
||||
const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
|
||||
if (LEVEL_ROLE_MAP[level]) {
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`);
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
||||
} else {
|
||||
return level;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import Modal from './Modal';
|
||||
import { getAddressType } from './UserAddress';
|
||||
import createRoom from './createRoom';
|
||||
import sdk from './';
|
||||
import dis from './dispatcher';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
export function inviteToRoom(roomId, addr) {
|
||||
@@ -28,7 +30,7 @@ export function inviteToRoom(roomId, addr) {
|
||||
|
||||
if (addrType == 'email') {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
return MatrixClientPeg.get().invite(roomId, addr);
|
||||
} else {
|
||||
throw new Error('Unsupported address');
|
||||
@@ -50,8 +52,8 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
||||
}
|
||||
|
||||
export function showStartChatInviteDialog() {
|
||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
||||
Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, {
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, {
|
||||
title: _t('Start a chat'),
|
||||
description: _t("Who would you like to communicate with?"),
|
||||
placeholder: _t("Email, name or matrix ID"),
|
||||
@@ -61,8 +63,8 @@ export function showStartChatInviteDialog() {
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId) {
|
||||
const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
|
||||
Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, {
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, {
|
||||
title: _t('Invite new room members'),
|
||||
description: _t('Who would you like to add to this room?'),
|
||||
button: _t('Send Invites'),
|
||||
@@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) {
|
||||
const addrTexts = addrs.map((addr) => addr.address);
|
||||
|
||||
if (_isDmChat(addrTexts)) {
|
||||
// Start a new DM chat
|
||||
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
||||
console.error(err.stack);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||
title: _t("Failed to invite user"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
const rooms = _getDirectMessageRooms(addrTexts[0]);
|
||||
if (rooms.length > 0) {
|
||||
// A Direct Message room already exists for this user, so select a
|
||||
// room from a list that is similar to the one in MemberInfo panel
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
"views.dialogs.ChatCreateOrReuseDialog",
|
||||
);
|
||||
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
||||
userId: addrTexts[0],
|
||||
onNewDMClick: () => {
|
||||
dis.dispatch({
|
||||
action: 'start_chat',
|
||||
user_id: addrTexts[0],
|
||||
});
|
||||
close(true);
|
||||
},
|
||||
onExistingRoomSelected: (roomId) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
close(true);
|
||||
},
|
||||
}).close;
|
||||
} else {
|
||||
// Start a new DM chat
|
||||
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||
title: _t("Failed to invite user"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Start multi user chat
|
||||
let room;
|
||||
@@ -127,7 +154,7 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) {
|
||||
}
|
||||
|
||||
function _isDmChat(addrTexts) {
|
||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0])) {
|
||||
if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) {
|
||||
return addrs;
|
||||
}
|
||||
|
||||
function _getDirectMessageRooms(addr) {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
const rooms = [];
|
||||
dmRooms.forEach((dmRoom) => {
|
||||
const room = MatrixClientPeg.get().getRoom(dmRoom);
|
||||
if (room) {
|
||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me.membership == 'join') {
|
||||
rooms.push(room);
|
||||
}
|
||||
}
|
||||
});
|
||||
return rooms;
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ export function isConfCallRoom(room, me, conferenceHandler) {
|
||||
|
||||
export function looksLikeDirectMessageRoom(room, me) {
|
||||
if (me.membership == "join" || me.membership === "ban" ||
|
||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey()))
|
||||
{
|
||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||
// Used to split rooms via tags
|
||||
const tagNames = Object.keys(room.tags);
|
||||
// Used for 1:1 direct chats
|
||||
|
||||
@@ -15,10 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
var request = require('browser-request');
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
const request = require('browser-request');
|
||||
|
||||
var SdkConfig = require('./SdkConfig');
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
const SdkConfig = require('./SdkConfig');
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
|
||||
class ScalarAuthClient {
|
||||
|
||||
@@ -38,7 +39,7 @@ class ScalarAuthClient {
|
||||
|
||||
// Returns a scalar_token string
|
||||
getScalarToken() {
|
||||
var tok = window.localStorage.getItem("mx_scalar_token");
|
||||
const tok = window.localStorage.getItem("mx_scalar_token");
|
||||
if (tok) return Promise.resolve(tok);
|
||||
|
||||
// No saved token, so do the dance to get one. First, we
|
||||
@@ -53,9 +54,9 @@ class ScalarAuthClient {
|
||||
}
|
||||
|
||||
exchangeForScalarToken(openid_token_object) {
|
||||
var defer = Promise.defer();
|
||||
const defer = Promise.defer();
|
||||
|
||||
var scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||
const scalar_rest_url = SdkConfig.get().integrations_rest_url;
|
||||
request({
|
||||
method: 'POST',
|
||||
uri: scalar_rest_url+'/register',
|
||||
@@ -76,10 +77,40 @@ class ScalarAuthClient {
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
getScalarPageTitle(url) {
|
||||
const defer = Promise.defer();
|
||||
|
||||
let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup';
|
||||
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
|
||||
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
|
||||
request({
|
||||
method: 'GET',
|
||||
uri: scalarPageLookupUrl,
|
||||
json: true,
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
} else if (response.statusCode / 100 !== 2) {
|
||||
defer.reject({statusCode: response.statusCode});
|
||||
} else if (!body) {
|
||||
defer.reject(new Error("Missing page title in response"));
|
||||
} else {
|
||||
let title = "";
|
||||
if (body.page_title_cache_item && body.page_title_cache_item.cached_title) {
|
||||
title = body.page_title_cache_item.cached_title;
|
||||
}
|
||||
defer.resolve(title);
|
||||
}
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
getScalarInterfaceUrlForRoom(roomId, screen, id) {
|
||||
var url = SdkConfig.get().integrations_ui_url;
|
||||
let url = SdkConfig.get().integrations_ui_url;
|
||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||
url += "&room_id=" + encodeURIComponent(roomId);
|
||||
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
||||
if (id) {
|
||||
url += '&integ_id=' + encodeURIComponent(id);
|
||||
}
|
||||
|
||||
@@ -356,16 +356,32 @@ function getWidgets(event, roomId) {
|
||||
}
|
||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
// Only return widgets which have required fields
|
||||
let widgetStateEvents = [];
|
||||
const widgetStateEvents = [];
|
||||
stateEvents.forEach((ev) => {
|
||||
if (ev.getContent().type && ev.getContent().url) {
|
||||
widgetStateEvents.push(ev.event); // return the raw event
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
sendResponse(event, widgetStateEvents);
|
||||
}
|
||||
|
||||
function getRoomEncState(event, roomId) {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
sendError(event, _t('This room is not recognised.'));
|
||||
return;
|
||||
}
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
||||
|
||||
sendResponse(event, roomIsEncrypted);
|
||||
}
|
||||
|
||||
function setPlumbingState(event, roomId, status) {
|
||||
if (typeof status !== 'string') {
|
||||
throw new Error('Plumbing state status should be a string');
|
||||
@@ -376,7 +392,7 @@ function setPlumbingState(event, roomId, status) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => {
|
||||
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => {
|
||||
sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
@@ -415,11 +431,11 @@ function setBotPower(event, roomId, userId, level) {
|
||||
}
|
||||
|
||||
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
||||
let powerEvent = new MatrixEvent(
|
||||
const powerEvent = new MatrixEvent(
|
||||
{
|
||||
type: "m.room.power_levels",
|
||||
content: powerLevels,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
|
||||
@@ -485,8 +501,7 @@ function canSendEvent(event, roomId) {
|
||||
let canSend = false;
|
||||
if (isState) {
|
||||
canSend = room.currentState.maySendStateEvent(evType, me);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
canSend = room.currentState.maySendEvent(evType, me);
|
||||
}
|
||||
|
||||
@@ -517,8 +532,8 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||
sendResponse(event, stateEvent.getContent());
|
||||
}
|
||||
|
||||
var currentRoomId = null;
|
||||
var currentRoomAlias = null;
|
||||
let currentRoomId = null;
|
||||
let currentRoomAlias = null;
|
||||
|
||||
// Listen for when a room is viewed
|
||||
dis.register(onAction);
|
||||
@@ -542,8 +557,16 @@ const onMessage = function(event) {
|
||||
//
|
||||
// All strings start with the empty string, so for sanity return if the length
|
||||
// of the event origin is 0.
|
||||
let url = SdkConfig.get().integrations_ui_url;
|
||||
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
|
||||
//
|
||||
// TODO -- Scalar postMessage API should be namespaced with event.data.api field
|
||||
// Fix following "if" statement to respond only to specific API messages.
|
||||
const url = SdkConfig.get().integrations_ui_url;
|
||||
if (
|
||||
event.origin.length === 0 ||
|
||||
!url.startsWith(event.origin) ||
|
||||
!event.data.action ||
|
||||
event.data.api // Ignore messages with specific API set
|
||||
) {
|
||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
@@ -594,6 +617,9 @@ const onMessage = function(event) {
|
||||
} else if (event.data.action === "get_widgets") {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_room_enc_state") {
|
||||
getRoomEncState(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
@@ -647,7 +673,7 @@ module.exports = {
|
||||
// Make an error so we get a stack trace
|
||||
const e = new Error(
|
||||
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
||||
" Negative count"
|
||||
" Negative count",
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULTS = {
|
||||
class SdkConfig {
|
||||
|
||||
static get() {
|
||||
return global.mxReactSdkConfig;
|
||||
return global.mxReactSdkConfig || {};
|
||||
}
|
||||
|
||||
static put(cfg) {
|
||||
|
||||
@@ -84,6 +84,9 @@ class Skinner {
|
||||
// behaviour with multiple copies of files etc. is erratic at best.
|
||||
// XXX: We can still end up with the same file twice in the resulting
|
||||
// JS bundle which is nonideal.
|
||||
// See https://derickbailey.com/2016/03/09/creating-a-true-singleton-in-node-js-with-es6-symbols/
|
||||
// or https://nodejs.org/api/modules.html#modules_module_caching_caveats
|
||||
// ("Modules are cached based on their resolved filename")
|
||||
if (global.mxSkinner === undefined) {
|
||||
global.mxSkinner = new Skinner();
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import Tinter from "./Tinter";
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
|
||||
|
||||
class Command {
|
||||
@@ -97,9 +98,7 @@ const commands = {
|
||||
colorScheme.secondary_color = matches[4];
|
||||
}
|
||||
return success(
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
roomId, "org.matrix.room.color_scheme", colorScheme,
|
||||
),
|
||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -240,6 +239,59 @@ const commands = {
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||
return success(
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||
title: _t("Ignored user"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
return success(
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||
title: _t("Unignored user"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Define the power level of a user
|
||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||
if (args) {
|
||||
@@ -292,6 +344,13 @@ const commands = {
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Open developer tools
|
||||
devtools: new Command("devtools", "", function(roomId) {
|
||||
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
||||
Modal.createDialog(DevtoolsDialog, { roomId });
|
||||
return success();
|
||||
}),
|
||||
|
||||
// Verify a user, device, and pubkey tuple
|
||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
||||
if (args) {
|
||||
|
||||
@@ -13,56 +13,67 @@ 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 MatrixClientPeg from "./MatrixClientPeg";
|
||||
import CallHandler from "./CallHandler";
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import CallHandler from './CallHandler';
|
||||
import { _t } from './languageHandler';
|
||||
import * as Roles from './Roles';
|
||||
|
||||
function textForMemberEvent(ev) {
|
||||
// XXX: SYJS-16 "sender is sometimes null for join messages"
|
||||
var senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
var targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||
var ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
var reason = ev.getContent().reason ? (
|
||||
_t('Reason') + ': ' + ev.getContent().reason
|
||||
) : "";
|
||||
switch (ev.getContent().membership) {
|
||||
case 'invite':
|
||||
var threePidContent = ev.getContent().third_party_invite;
|
||||
const senderName = ev.sender ? ev.sender.name : ev.getSender();
|
||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const content = ev.getContent();
|
||||
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
||||
switch (content.membership) {
|
||||
case 'invite': {
|
||||
const threePidContent = content.third_party_invite;
|
||||
if (threePidContent) {
|
||||
if (threePidContent.display_name) {
|
||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
|
||||
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
||||
targetName,
|
||||
displayName: threePidContent.display_name,
|
||||
});
|
||||
} else {
|
||||
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
|
||||
return _t('%(targetName)s accepted an invitation.', {targetName});
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
|
||||
}
|
||||
else {
|
||||
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||
return _t('%(senderName)s requested a VoIP conference.', {senderName});
|
||||
} else {
|
||||
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'ban':
|
||||
return _t(
|
||||
'%(senderName)s banned %(targetName)s.',
|
||||
{senderName: senderName, targetName: targetName}
|
||||
) + ' ' + reason;
|
||||
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
case 'join':
|
||||
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
|
||||
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
|
||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
|
||||
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
|
||||
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
|
||||
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
|
||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
|
||||
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
|
||||
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
|
||||
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
|
||||
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
|
||||
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
|
||||
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
|
||||
if (prevContent && prevContent.membership === 'join') {
|
||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
||||
senderName,
|
||||
oldDisplayName: prevContent.displayname,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} else if (!prevContent.displayname && content.displayname) {
|
||||
return _t('%(senderName)s set their display name to %(displayName)s.', {
|
||||
senderName,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
} else if (prevContent.displayname && !content.displayname) {
|
||||
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
||||
senderName,
|
||||
oldDisplayName: prevContent.displayname,
|
||||
});
|
||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||
return _t('%(senderName)s removed their profile picture.', {senderName});
|
||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||
prevContent.avatar_url !== content.avatar_url) {
|
||||
return _t('%(senderName)s changed their profile picture.', {senderName});
|
||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||
return _t('%(senderName)s set a profile picture.', {senderName});
|
||||
} else {
|
||||
// suppress null rejoins
|
||||
return '';
|
||||
@@ -71,82 +82,78 @@ function textForMemberEvent(ev) {
|
||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||
return _t('VoIP conference started.');
|
||||
}
|
||||
else {
|
||||
return _t('%(targetName)s joined the room.', {targetName: targetName});
|
||||
} else {
|
||||
return _t('%(targetName)s joined the room.', {targetName});
|
||||
}
|
||||
}
|
||||
case 'leave':
|
||||
if (ev.getSender() === ev.getStateKey()) {
|
||||
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
|
||||
return _t('VoIP conference finished.');
|
||||
} else if (prevContent.membership === "invite") {
|
||||
return _t('%(targetName)s rejected the invitation.', {targetName});
|
||||
} else {
|
||||
return _t('%(targetName)s left the room.', {targetName});
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "invite") {
|
||||
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
|
||||
}
|
||||
else {
|
||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
||||
}
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "ban") {
|
||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "join") {
|
||||
return _t(
|
||||
'%(senderName)s kicked %(targetName)s.',
|
||||
{senderName: senderName, targetName: targetName}
|
||||
) + ' ' + reason;
|
||||
}
|
||||
else if (ev.getPrevContent().membership === "invite") {
|
||||
return _t(
|
||||
'%(senderName)s withdrew %(targetName)s\'s invitation.',
|
||||
{senderName: senderName, targetName: targetName}
|
||||
) + ' ' + reason;
|
||||
}
|
||||
else {
|
||||
return _t('%(targetName)s left the room.', {targetName: targetName});
|
||||
} else if (prevContent.membership === "ban") {
|
||||
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
||||
} else if (prevContent.membership === "join") {
|
||||
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
|
||||
} else if (prevContent.membership === "invite") {
|
||||
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
||||
senderName,
|
||||
targetName,
|
||||
}) + ' ' + reason;
|
||||
} else {
|
||||
return _t('%(targetName)s left the room.', {targetName});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function textForTopicEvent(ev) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
|
||||
senderDisplayName,
|
||||
topic: ev.getContent().topic,
|
||||
});
|
||||
}
|
||||
|
||||
function textForRoomNameEvent(ev) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
|
||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
|
||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||
}
|
||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
|
||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||
senderDisplayName,
|
||||
roomName: ev.getContent().name,
|
||||
});
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev) {
|
||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
var message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
if (ev.getContent().msgtype === "m.emote") {
|
||||
message = "* " + senderDisplayName + " " + message;
|
||||
} else if (ev.getContent().msgtype === "m.image") {
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
|
||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
||||
}
|
||||
|
||||
function textForCallHangupEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const eventContent = event.getContent();
|
||||
let reason = "";
|
||||
if(!MatrixClientPeg.get().supportsVoip()) {
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
reason = _t('(not supported by this browser)');
|
||||
} else if(eventContent.reason) {
|
||||
} else if (eventContent.reason) {
|
||||
if (eventContent.reason === "ice_failed") {
|
||||
reason = _t('(could not connect media)');
|
||||
} else if (eventContent.reason === "invite_timeout") {
|
||||
@@ -159,48 +166,52 @@ function textForCallHangupEvent(event) {
|
||||
}
|
||||
|
||||
function textForCallInviteEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
var type = "voice";
|
||||
let callType = "voice";
|
||||
if (event.getContent().offer && event.getContent().offer.sdp &&
|
||||
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
|
||||
type = "video";
|
||||
callType = "video";
|
||||
}
|
||||
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
|
||||
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
|
||||
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
|
||||
}
|
||||
|
||||
function textForThreePidInviteEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
|
||||
senderName,
|
||||
targetDisplayName: event.getContent().display_name,
|
||||
});
|
||||
}
|
||||
|
||||
function textForHistoryVisibilityEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||
var vis = event.getContent().history_visibility;
|
||||
// XXX: This i18n just isn't going to work for languages with different sentence structure.
|
||||
var text = _t('%(senderName)s made future room history visible to', {senderName: senderName}) + ' ';
|
||||
if (vis === "invited") {
|
||||
text += _t('all room members, from the point they are invited') + '.';
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
switch (event.getContent().history_visibility) {
|
||||
case 'invited':
|
||||
return _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they are invited.', {senderName});
|
||||
case 'joined':
|
||||
return _t('%(senderName)s made future room history visible to all room members, '
|
||||
+ 'from the point they joined.', {senderName});
|
||||
case 'shared':
|
||||
return _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
||||
case 'world_readable':
|
||||
return _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
||||
default:
|
||||
return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||
senderName,
|
||||
visibility: event.getContent().history_visibility,
|
||||
});
|
||||
}
|
||||
else if (vis === "joined") {
|
||||
text += _t('all room members, from the point they joined') + '.';
|
||||
}
|
||||
else if (vis === "shared") {
|
||||
text += _t('all room members') + '.';
|
||||
}
|
||||
else if (vis === "world_readable") {
|
||||
text += _t('anyone') + '.';
|
||||
}
|
||||
else {
|
||||
text += ' ' + _t('unknown') + ' (' + vis + ').';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function textForEncryptionEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
|
||||
senderName,
|
||||
algorithm: event.getContent().algorithm,
|
||||
});
|
||||
}
|
||||
|
||||
// Currently will only display a change if a user's power level is changed
|
||||
@@ -211,18 +222,18 @@ function textForPowerEvent(event) {
|
||||
}
|
||||
const userDefault = event.getContent().users_default || 0;
|
||||
// Construct set of userIds
|
||||
let users = [];
|
||||
const users = [];
|
||||
Object.keys(event.getContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
},
|
||||
);
|
||||
Object.keys(event.getPrevContent().users).forEach(
|
||||
(userId) => {
|
||||
if (users.indexOf(userId) === -1) users.push(userId);
|
||||
}
|
||||
},
|
||||
);
|
||||
let diff = [];
|
||||
const diff = [];
|
||||
// XXX: This is also surely broken for i18n
|
||||
users.forEach((userId) => {
|
||||
// Previous power level
|
||||
@@ -231,11 +242,11 @@ function textForPowerEvent(event) {
|
||||
const to = event.getContent().users[userId];
|
||||
if (to !== from) {
|
||||
diff.push(
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId: userId,
|
||||
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
|
||||
userId,
|
||||
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
|
||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
|
||||
})
|
||||
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -243,16 +254,22 @@ function textForPowerEvent(event) {
|
||||
return '';
|
||||
}
|
||||
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
|
||||
senderName: senderName,
|
||||
powerLevelDiffText: diff.join(", ")
|
||||
senderName,
|
||||
powerLevelDiffText: diff.join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
function textForPinnedEvent(event) {
|
||||
const senderName = event.getSender();
|
||||
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
||||
}
|
||||
|
||||
function textForWidgetEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||
const previousContent = event.getPrevContent() || {};
|
||||
const senderName = event.getSender();
|
||||
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
||||
const {name, type, url} = event.getContent() || {};
|
||||
let widgetName = name || previousContent.name || type || previousContent.type || '';
|
||||
|
||||
let widgetName = name || prevName || type || prevType || '';
|
||||
// Apply sentence case to widget name
|
||||
if (widgetName && widgetName.length > 0) {
|
||||
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' ';
|
||||
@@ -261,9 +278,15 @@ function textForWidgetEvent(event) {
|
||||
// If the widget was removed, its content should be {}, but this is sufficiently
|
||||
// equivalent to that condition.
|
||||
if (url) {
|
||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
if (prevUrl) {
|
||||
return _t('%(widgetName)s widget modified by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
} else {
|
||||
return _t('%(widgetName)s widget added by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return _t('%(widgetName)s widget removed by %(senderName)s', {
|
||||
widgetName, senderName,
|
||||
@@ -271,26 +294,30 @@ function textForWidgetEvent(event) {
|
||||
}
|
||||
}
|
||||
|
||||
var handlers = {
|
||||
const handlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
};
|
||||
|
||||
const stateHandlers = {
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
'm.room.member': textForMemberEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent,
|
||||
'm.room.history_visibility': textForHistoryVisibilityEvent,
|
||||
'm.room.encryption': textForEncryptionEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
'm.room.pinned_events': textForPinnedEvent,
|
||||
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
textForEvent: function(ev) {
|
||||
var hdlr = handlers[ev.getType()];
|
||||
if (!hdlr) return "";
|
||||
return hdlr(ev);
|
||||
}
|
||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||
if (handler) return handler(ev);
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
513
src/Tinter.js
513
src/Tinter.js
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,148 +15,125 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// FIXME: these vars should be bundled up and attached to
|
||||
// module.exports otherwise this will break when included by both
|
||||
// react-sdk and apps layered on top.
|
||||
const DEBUG = 0;
|
||||
|
||||
var DEBUG = 0;
|
||||
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
||||
function colorToRgb(color) {
|
||||
if (!color) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
// The colour keys to be replaced as referred to in CSS
|
||||
var keyRgb = [
|
||||
"rgb(118, 207, 166)", // Vector Green
|
||||
"rgb(234, 245, 240)", // Vector Light Green
|
||||
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
|
||||
];
|
||||
|
||||
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
||||
// x * 118 + (1 - x) * 255 = 234
|
||||
// x * 118 + 255 - 255 * x = 234
|
||||
// x * 118 - x * 255 = 234 - 255
|
||||
// (255 - 118) x = 255 - 234
|
||||
// x = (255 - 234) / (255 - 118) = 0.16
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
var keyHex = [
|
||||
"#76CFA6", // Vector Green
|
||||
"#EAF5F0", // Vector Light Green
|
||||
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// cache of our replacement colours
|
||||
// defaults to our keys.
|
||||
var colors = [
|
||||
keyHex[0],
|
||||
keyHex[1],
|
||||
keyHex[2],
|
||||
keyHex[3],
|
||||
];
|
||||
|
||||
var cssFixups = [
|
||||
// {
|
||||
// style: a style object that should be fixed up taken from a stylesheet
|
||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||
// index: ordinal of primary, secondary or tertiary
|
||||
// }
|
||||
];
|
||||
|
||||
// CSS attributes to be fixed up
|
||||
var cssAttrs = [
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
var svgAttrs = [
|
||||
"fill",
|
||||
"stroke",
|
||||
];
|
||||
|
||||
var cached = false;
|
||||
|
||||
function calcCssFixups() {
|
||||
if (DEBUG) console.log("calcSvgFixups start");
|
||||
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||
var ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
// privacybadger inserting its own stylesheet in there with a
|
||||
// resource:// URI or something which results in a XSS error.
|
||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||
// ...except some browsers apparently return stylesheets without
|
||||
// hrefs, which we have no choice but ignore right now
|
||||
|
||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||
// here?
|
||||
//
|
||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||
// are other CSS files affecting the doc don't we want to apply the
|
||||
// same transformations to them?
|
||||
//
|
||||
// Iterating through the CSS looking for matches to hack on feels
|
||||
// pretty horrible anyway. And what if the application skin doesn't use
|
||||
// Vector Green as its primary color?
|
||||
|
||||
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
|
||||
|
||||
if (!ss.cssRules) continue;
|
||||
for (var j = 0; j < ss.cssRules.length; j++) {
|
||||
var rule = ss.cssRules[j];
|
||||
if (!rule.style) continue;
|
||||
for (var k = 0; k < cssAttrs.length; k++) {
|
||||
var attr = cssAttrs[k];
|
||||
for (var l = 0; l < keyRgb.length; l++) {
|
||||
if (rule.style[attr] === keyRgb[l]) {
|
||||
cssFixups.push({
|
||||
style: rule.style,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (color[0] === '#') {
|
||||
color = color.slice(1);
|
||||
if (color.length === 3) {
|
||||
color = color[0] + color[0] +
|
||||
color[1] + color[1] +
|
||||
color[2] + color[2];
|
||||
}
|
||||
const val = parseInt(color, 16);
|
||||
const r = (val >> 16) & 255;
|
||||
const g = (val >> 8) & 255;
|
||||
const b = val & 255;
|
||||
return [r, g, b];
|
||||
} else {
|
||||
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
|
||||
if (match) {
|
||||
return [
|
||||
parseInt(match[1]),
|
||||
parseInt(match[2]),
|
||||
parseInt(match[3]),
|
||||
];
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
function applyCssFixups() {
|
||||
if (DEBUG) console.log("applyCssFixups start");
|
||||
for (var i = 0; i < cssFixups.length; i++) {
|
||||
var cssFixup = cssFixups[i];
|
||||
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
||||
}
|
||||
if (DEBUG) console.log("applyCssFixups end");
|
||||
}
|
||||
|
||||
function hexToRgb(color) {
|
||||
if (color[0] === '#') color = color.slice(1);
|
||||
if (color.length === 3) {
|
||||
color = color[0] + color[0] +
|
||||
color[1] + color[1] +
|
||||
color[2] + color[2];
|
||||
}
|
||||
var val = parseInt(color, 16);
|
||||
var r = (val >> 16) & 255;
|
||||
var g = (val >> 8) & 255;
|
||||
var b = val & 255;
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
function rgbToHex(rgb) {
|
||||
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||
// utility to turn [red,green,blue] into #rrggbb
|
||||
function rgbToColor(rgb) {
|
||||
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||
return '#' + (0x1000000 + val).toString(16).slice(1);
|
||||
}
|
||||
|
||||
// List of functions to call when the tint changes.
|
||||
const tintables = [];
|
||||
class Tinter {
|
||||
constructor() {
|
||||
// The default colour keys to be replaced as referred to in CSS
|
||||
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
|
||||
this.keyRgb = [
|
||||
"rgb(118, 207, 166)", // Vector Green
|
||||
"rgb(234, 245, 240)", // Vector Light Green
|
||||
"rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
||||
];
|
||||
|
||||
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
||||
// x * 118 + (1 - x) * 255 = 234
|
||||
// x * 118 + 255 - 255 * x = 234
|
||||
// x * 118 - x * 255 = 234 - 255
|
||||
// (255 - 118) x = 255 - 234
|
||||
// x = (255 - 234) / (255 - 118) = 0.16
|
||||
|
||||
// The colour keys to be replaced as referred to in SVGs
|
||||
this.keyHex = [
|
||||
"#76CFA6", // Vector Green
|
||||
"#EAF5F0", // Vector Light Green
|
||||
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
||||
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
||||
];
|
||||
|
||||
// track the replacement colours actually being used
|
||||
// defaults to our keys.
|
||||
this.colors = [
|
||||
this.keyHex[0],
|
||||
this.keyHex[1],
|
||||
this.keyHex[2],
|
||||
this.keyHex[3],
|
||||
this.keyHex[4],
|
||||
];
|
||||
|
||||
// track the most current tint request inputs (which may differ from the
|
||||
// end result stored in this.colors
|
||||
this.currentTint = [
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
];
|
||||
|
||||
this.cssFixups = [
|
||||
// { theme: {
|
||||
// style: a style object that should be fixed up taken from a stylesheet
|
||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||
// index: ordinal of primary, secondary or tertiary
|
||||
// },
|
||||
// }
|
||||
];
|
||||
|
||||
// CSS attributes to be fixed up
|
||||
this.cssAttrs = [
|
||||
"color",
|
||||
"backgroundColor",
|
||||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
this.svgAttrs = [
|
||||
"fill",
|
||||
"stroke",
|
||||
];
|
||||
|
||||
// List of functions to call when the tint changes.
|
||||
this.tintables = [];
|
||||
|
||||
// the currently loaded theme (if any)
|
||||
this.theme = undefined;
|
||||
|
||||
// whether to force a tint (e.g. after changing theme)
|
||||
this.forceTint = false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Register a callback to fire when the tint changes.
|
||||
* This is used to rewrite the tintable SVGs with the new tint.
|
||||
@@ -167,111 +145,273 @@ module.exports = {
|
||||
*
|
||||
* @param {Function} tintable Function to call when the tint changes.
|
||||
*/
|
||||
registerTintable : function(tintable) {
|
||||
tintables.push(tintable);
|
||||
},
|
||||
registerTintable(tintable) {
|
||||
this.tintables.push(tintable);
|
||||
}
|
||||
|
||||
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||
getKeyRgb() {
|
||||
return this.keyRgb;
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
calcCssFixups();
|
||||
cached = true;
|
||||
tint(primaryColor, secondaryColor, tertiaryColor) {
|
||||
this.currentTint[0] = primaryColor;
|
||||
this.currentTint[1] = secondaryColor;
|
||||
this.currentTint[2] = tertiaryColor;
|
||||
|
||||
this.calcCssFixups();
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint(" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
if (!primaryColor) {
|
||||
primaryColor = "#76CFA6"; // Vector green
|
||||
secondaryColor = "#EAF5F0"; // Vector light green
|
||||
primaryColor = this.keyRgb[0];
|
||||
secondaryColor = this.keyRgb[1];
|
||||
tertiaryColor = this.keyRgb[2];
|
||||
}
|
||||
|
||||
if (!secondaryColor) {
|
||||
const x = 0.16; // average weighting factor calculated from vector green & light green
|
||||
var rgb = hexToRgb(primaryColor);
|
||||
const rgb = colorToRgb(primaryColor);
|
||||
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
||||
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
||||
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
||||
secondaryColor = rgbToHex(rgb);
|
||||
secondaryColor = rgbToColor(rgb);
|
||||
}
|
||||
|
||||
if (!tertiaryColor) {
|
||||
const x = 0.19;
|
||||
var rgb1 = hexToRgb(primaryColor);
|
||||
var rgb2 = hexToRgb(secondaryColor);
|
||||
const rgb1 = colorToRgb(primaryColor);
|
||||
const rgb2 = colorToRgb(secondaryColor);
|
||||
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
||||
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
||||
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
||||
tertiaryColor = rgbToHex(rgb1);
|
||||
tertiaryColor = rgbToColor(rgb1);
|
||||
}
|
||||
|
||||
if (colors[0] === primaryColor &&
|
||||
colors[1] === secondaryColor &&
|
||||
colors[2] === tertiaryColor)
|
||||
{
|
||||
if (this.forceTint == false &&
|
||||
this.colors[0] === primaryColor &&
|
||||
this.colors[1] === secondaryColor &&
|
||||
this.colors[2] === tertiaryColor) {
|
||||
return;
|
||||
}
|
||||
|
||||
colors[0] = primaryColor;
|
||||
colors[1] = secondaryColor;
|
||||
colors[2] = tertiaryColor;
|
||||
this.forceTint = false;
|
||||
|
||||
if (DEBUG) console.log("Tinter.tint");
|
||||
this.colors[0] = primaryColor;
|
||||
this.colors[1] = secondaryColor;
|
||||
this.colors[2] = tertiaryColor;
|
||||
|
||||
if (DEBUG) {
|
||||
console.log("Tinter.tint final: (" + primaryColor + ", " +
|
||||
secondaryColor + ", " +
|
||||
tertiaryColor + ")");
|
||||
}
|
||||
|
||||
// go through manually fixing up the stylesheets.
|
||||
applyCssFixups();
|
||||
this.applyCssFixups();
|
||||
|
||||
// tell all the SVGs to go fix themselves up
|
||||
// we don't do this as a dispatch otherwise it will visually lag
|
||||
tintables.forEach(function(tintable) {
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
tintSvgWhite(whiteColor) {
|
||||
this.currentTint[3] = whiteColor;
|
||||
|
||||
tintSvgWhite: function(whiteColor) {
|
||||
if (!whiteColor) {
|
||||
whiteColor = colors[3];
|
||||
whiteColor = this.colors[3];
|
||||
}
|
||||
if (colors[3] === whiteColor) {
|
||||
if (this.colors[3] === whiteColor) {
|
||||
return;
|
||||
}
|
||||
colors[3] = whiteColor;
|
||||
tintables.forEach(function(tintable) {
|
||||
this.colors[3] = whiteColor;
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
tintSvgBlack(blackColor) {
|
||||
this.currentTint[4] = blackColor;
|
||||
|
||||
if (!blackColor) {
|
||||
blackColor = this.colors[4];
|
||||
}
|
||||
if (this.colors[4] === blackColor) {
|
||||
return;
|
||||
}
|
||||
this.colors[4] = blackColor;
|
||||
this.tintables.forEach(function(tintable) {
|
||||
tintable();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setTheme(theme) {
|
||||
console.trace("setTheme " + theme);
|
||||
this.theme = theme;
|
||||
|
||||
// update keyRgb from the current theme CSS itself, if it defines it
|
||||
if (document.getElementById('mx_theme_accentColor')) {
|
||||
this.keyRgb[0] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_accentColor')).color;
|
||||
}
|
||||
if (document.getElementById('mx_theme_secondaryAccentColor')) {
|
||||
this.keyRgb[1] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_secondaryAccentColor')).color;
|
||||
}
|
||||
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
|
||||
this.keyRgb[2] = window.getComputedStyle(
|
||||
document.getElementById('mx_theme_tertiaryAccentColor')).color;
|
||||
}
|
||||
|
||||
this.calcCssFixups();
|
||||
this.forceTint = true;
|
||||
|
||||
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
|
||||
|
||||
if (theme === 'dark') {
|
||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||
// XXX: obviously this shouldn't be hardcoded here.
|
||||
this.tintSvgWhite('#2d2d2d');
|
||||
this.tintSvgBlack('#dddddd');
|
||||
} else {
|
||||
this.tintSvgWhite('#ffffff');
|
||||
this.tintSvgBlack('#000000');
|
||||
}
|
||||
}
|
||||
|
||||
calcCssFixups() {
|
||||
// cache our fixups
|
||||
if (this.cssFixups[this.theme]) return;
|
||||
|
||||
if (DEBUG) {
|
||||
console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
||||
document.styleSheets.length +
|
||||
" stylesheets)");
|
||||
}
|
||||
|
||||
this.cssFixups[this.theme] = [];
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (!ss) continue; // well done safari >:(
|
||||
// Chromium apparently sometimes returns null here; unsure why.
|
||||
// see $14534907369972FRXBx:matrix.org in HQ
|
||||
// ...ah, it's because there's a third party extension like
|
||||
// privacybadger inserting its own stylesheet in there with a
|
||||
// resource:// URI or something which results in a XSS error.
|
||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
||||
// ...except some browsers apparently return stylesheets without
|
||||
// hrefs, which we have no choice but ignore right now
|
||||
|
||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
||||
// here?
|
||||
//
|
||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
||||
// are other CSS files affecting the doc don't we want to apply the
|
||||
// same transformations to them?
|
||||
//
|
||||
// Iterating through the CSS looking for matches to hack on feels
|
||||
// pretty horrible anyway. And what if the application skin doesn't use
|
||||
// Vector Green as its primary color?
|
||||
// --richvdh
|
||||
|
||||
// Yes, tinting assumes that you are using the Riot skin for now.
|
||||
// The right solution will be to move the CSS over to react-sdk.
|
||||
// And yes, the default assets for the base skin might as well use
|
||||
// Vector Green as any other colour.
|
||||
// --matthew
|
||||
|
||||
if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
||||
if (ss.disabled) continue;
|
||||
if (!ss.cssRules) continue;
|
||||
|
||||
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
||||
|
||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
||||
const rule = ss.cssRules[j];
|
||||
if (!rule.style) continue;
|
||||
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
||||
for (let k = 0; k < this.cssAttrs.length; k++) {
|
||||
const attr = this.cssAttrs[k];
|
||||
for (let l = 0; l < this.keyRgb.length; l++) {
|
||||
if (rule.style[attr] === this.keyRgb[l]) {
|
||||
this.cssFixups[this.theme].push({
|
||||
style: rule.style,
|
||||
attr: attr,
|
||||
index: l,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log("calcCssFixups end (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
}
|
||||
|
||||
applyCssFixups() {
|
||||
if (DEBUG) {
|
||||
console.log("applyCssFixups start (" +
|
||||
this.cssFixups[this.theme].length +
|
||||
" fixups)");
|
||||
}
|
||||
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
|
||||
const cssFixup = this.cssFixups[this.theme][i];
|
||||
try {
|
||||
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
|
||||
} catch (e) {
|
||||
// Firefox Quantum explodes if you manually edit the CSS in the
|
||||
// inspector and then try to do a tint, as apparently all the
|
||||
// fixups are then stale.
|
||||
console.error("Failed to apply cssFixup in Tinter! ", e.name);
|
||||
}
|
||||
}
|
||||
if (DEBUG) console.log("applyCssFixups end");
|
||||
}
|
||||
|
||||
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
||||
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
||||
// keeping it here for now.
|
||||
calcSvgFixups: function(svgs) {
|
||||
calcSvgFixups(svgs) {
|
||||
// go through manually fixing up SVG colours.
|
||||
// we could do this by stylesheets, but keeping the stylesheets
|
||||
// updated would be a PITA, so just brute-force search for the
|
||||
// key colour; cache the element and apply.
|
||||
|
||||
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
||||
var fixups = [];
|
||||
for (var i = 0; i < svgs.length; i++) {
|
||||
var svgDoc;
|
||||
const fixups = [];
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
let svgDoc;
|
||||
try {
|
||||
svgDoc = svgs[i].contentDocument;
|
||||
}
|
||||
catch(e) {
|
||||
var msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
||||
} catch (e) {
|
||||
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
||||
if (e.message) {
|
||||
msg += e.message;
|
||||
}
|
||||
if (e.stack) {
|
||||
msg += ' | stack: ' + e.stack;
|
||||
}
|
||||
console.error(e);
|
||||
console.error(msg);
|
||||
}
|
||||
if (!svgDoc) continue;
|
||||
var tags = svgDoc.getElementsByTagName("*");
|
||||
for (var j = 0; j < tags.length; j++) {
|
||||
var tag = tags[j];
|
||||
for (var k = 0; k < svgAttrs.length; k++) {
|
||||
var attr = svgAttrs[k];
|
||||
for (var l = 0; l < keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
||||
const tags = svgDoc.getElementsByTagName("*");
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
const tag = tags[j];
|
||||
for (let k = 0; k < this.svgAttrs.length; k++) {
|
||||
const attr = this.svgAttrs[k];
|
||||
for (let l = 0; l < this.keyHex.length; l++) {
|
||||
if (tag.getAttribute(attr) &&
|
||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
||||
fixups.push({
|
||||
node: tag,
|
||||
attr: attr,
|
||||
@@ -285,14 +425,19 @@ module.exports = {
|
||||
if (DEBUG) console.log("calcSvgFixups end");
|
||||
|
||||
return fixups;
|
||||
},
|
||||
}
|
||||
|
||||
applySvgFixups: function(fixups) {
|
||||
applySvgFixups(fixups) {
|
||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
||||
for (var i = 0; i < fixups.length; i++) {
|
||||
var svgFixup = fixups[i];
|
||||
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
||||
for (let i = 0; i < fixups.length; i++) {
|
||||
const svgFixup = fixups[i];
|
||||
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
|
||||
}
|
||||
if (DEBUG) console.log("applySvgFixups end");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (global.singletonTinter === undefined) {
|
||||
global.singletonTinter = new Tinter();
|
||||
}
|
||||
export default global.singletonTinter;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
import UserSettingsStore from './UserSettingsStore';
|
||||
const MatrixClientPeg = require('./MatrixClientPeg');
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
var sdk = require('./index');
|
||||
const sdk = require('./index');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -34,17 +33,17 @@ module.exports = {
|
||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||
return false;
|
||||
}
|
||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
return EventTile.haveTileForEvent(ev);
|
||||
},
|
||||
|
||||
doesRoomHaveUnreadMessages: function(room) {
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
// get the most recent read receipt sent by our account.
|
||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||
// despite the name of the method :((
|
||||
var readUpToId = room.getEventReadUpTo(myUserId);
|
||||
const readUpToId = room.getEventReadUpTo(myUserId);
|
||||
|
||||
// as we don't send RRs for our own messages, make sure we special case that
|
||||
// if *we* sent the last message into the room, we consider it not unread!
|
||||
@@ -54,8 +53,7 @@ module.exports = {
|
||||
// https://github.com/vector-im/riot-web/issues/3363
|
||||
if (room.timeline.length &&
|
||||
room.timeline[room.timeline.length - 1].sender &&
|
||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
|
||||
{
|
||||
room.timeline[room.timeline.length - 1].sender.userId === myUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,17 +63,16 @@ module.exports = {
|
||||
// we have and the read receipt. We could fetch more history to try & find out,
|
||||
// but currently we just guess.
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
// Loop through messages, starting with the most recent...
|
||||
for (var i = room.timeline.length - 1; i >= 0; --i) {
|
||||
var ev = room.timeline[i];
|
||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||
const ev = room.timeline[i];
|
||||
|
||||
if (ev.getId() == readUpToId) {
|
||||
// If we've read up to this event, there's nothing more recents
|
||||
// that counts and we can stop looking because the user's read
|
||||
// this and everything before.
|
||||
return false;
|
||||
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) {
|
||||
} else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) {
|
||||
// We've found a message that counts before we hit
|
||||
// the read marker, so this room is definitely unread.
|
||||
return true;
|
||||
@@ -86,5 +83,5 @@ module.exports = {
|
||||
// is unread on the theory that false positives are better than
|
||||
// false negatives here.
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,11 +16,12 @@ limitations under the License.
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
|
||||
const mxidRegex = /^@\S+:\S+$/;
|
||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
export const addressTypes = [
|
||||
'mx', 'email',
|
||||
'mx-user-id', 'mx-room-id', 'email',
|
||||
];
|
||||
|
||||
// PropType definition for an object describing
|
||||
@@ -41,13 +42,16 @@ export const UserAddressType = PropTypes.shape({
|
||||
|
||||
export function getAddressType(inputText) {
|
||||
const isEmailAddress = emailRegex.test(inputText);
|
||||
const isMatrixId = mxidRegex.test(inputText);
|
||||
const isUserId = mxUserIdRegex.test(inputText);
|
||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
||||
|
||||
// sanity check the input for user IDs
|
||||
if (isEmailAddress) {
|
||||
return 'email';
|
||||
} else if (isMatrixId) {
|
||||
return 'mx';
|
||||
} else if (isUserId) {
|
||||
return 'mx-user-id';
|
||||
} else if (isRoomId) {
|
||||
return 'mx-room-id';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,30 +17,11 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import Notifier from './Notifier';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
/*
|
||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||
*/
|
||||
|
||||
export default {
|
||||
LABS_FEATURES: [
|
||||
{
|
||||
name: "-",
|
||||
id: 'matrix_apps',
|
||||
default: true,
|
||||
|
||||
// XXX: Always use default, ignore localStorage and remove from labs
|
||||
override: true,
|
||||
},
|
||||
],
|
||||
|
||||
// horrible but it works. The locality makes this somewhat more palatable.
|
||||
doTranslations: function() {
|
||||
this.LABS_FEATURES[0].name = _t("Matrix Apps");
|
||||
},
|
||||
|
||||
loadProfileInfo: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId);
|
||||
@@ -62,25 +44,6 @@ export default {
|
||||
// TODO
|
||||
},
|
||||
|
||||
getEnableNotifications: function() {
|
||||
return Notifier.isEnabled();
|
||||
},
|
||||
|
||||
setEnableNotifications: function(enable) {
|
||||
if (!Notifier.supportsDesktopNotifications()) {
|
||||
return;
|
||||
}
|
||||
Notifier.setEnabled(enable);
|
||||
},
|
||||
|
||||
getEnableAudioNotifications: function() {
|
||||
return Notifier.isAudioEnabled();
|
||||
},
|
||||
|
||||
setEnableAudioNotifications: function(enable) {
|
||||
Notifier.setAudioEnabled(enable);
|
||||
},
|
||||
|
||||
changePassword: function(oldPassword, newPassword) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
@@ -127,83 +90,4 @@ export default {
|
||||
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
|
||||
});
|
||||
},
|
||||
|
||||
getUrlPreviewsDisabled: function() {
|
||||
const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
|
||||
return (event && event.getContent().disable);
|
||||
},
|
||||
|
||||
setUrlPreviewsDisabled: function(disabled) {
|
||||
// FIXME: handle errors
|
||||
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
|
||||
disable: disabled,
|
||||
});
|
||||
},
|
||||
|
||||
getSyncedSettings: function() {
|
||||
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
|
||||
return event ? event.getContent() : {};
|
||||
},
|
||||
|
||||
getSyncedSetting: function(type, defaultValue = null) {
|
||||
const settings = this.getSyncedSettings();
|
||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||
},
|
||||
|
||||
setSyncedSetting: function(type, value) {
|
||||
const settings = this.getSyncedSettings();
|
||||
settings[type] = value;
|
||||
// FIXME: handle errors
|
||||
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
|
||||
},
|
||||
|
||||
getLocalSettings: function() {
|
||||
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
|
||||
return JSON.parse(localSettingsString);
|
||||
},
|
||||
|
||||
getLocalSetting: function(type, defaultValue = null) {
|
||||
const settings = this.getLocalSettings();
|
||||
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
|
||||
},
|
||||
|
||||
setLocalSetting: function(type, value) {
|
||||
const settings = this.getLocalSettings();
|
||||
settings[type] = value;
|
||||
// FIXME: handle errors
|
||||
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
|
||||
},
|
||||
|
||||
getFeatureById(feature: string) {
|
||||
for (let i = 0; i < this.LABS_FEATURES.length; i++) {
|
||||
const f = this.LABS_FEATURES[i];
|
||||
if (f.id === feature) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
isFeatureEnabled: function(featureId: string): boolean {
|
||||
// Disable labs for guests.
|
||||
if (MatrixClientPeg.get().isGuest()) return false;
|
||||
|
||||
const feature = this.getFeatureById(featureId);
|
||||
if (!feature) {
|
||||
console.warn(`Unknown feature "${featureId}"`);
|
||||
return false;
|
||||
}
|
||||
// Return the default if this feature has an override to be the default value or
|
||||
// if the feature has never been toggled and is therefore not in localStorage
|
||||
if (Object.keys(feature).includes('override') ||
|
||||
localStorage.getItem(`mx_labs_feature_${featureId}`) === null
|
||||
) {
|
||||
return feature.default;
|
||||
}
|
||||
return localStorage.getItem(`mx_labs_feature_${featureId}`) === 'true';
|
||||
},
|
||||
|
||||
setFeatureEnabled: function(featureId: string, enabled: boolean) {
|
||||
localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var React = require('react');
|
||||
var ReactDom = require('react-dom');
|
||||
var Velocity = require('velocity-vector');
|
||||
const React = require('react');
|
||||
const ReactDom = require('react-dom');
|
||||
const Velocity = require('velocity-vector');
|
||||
|
||||
/**
|
||||
* The Velociraptor contains components and animates transitions with velocity.
|
||||
@@ -46,13 +46,13 @@ module.exports = React.createClass({
|
||||
* update `this.children` according to the new list of children given
|
||||
*/
|
||||
_updateChildren: function(newChildren) {
|
||||
var self = this;
|
||||
var oldChildren = this.children || {};
|
||||
const self = this;
|
||||
const oldChildren = this.children || {};
|
||||
this.children = {};
|
||||
React.Children.toArray(newChildren).forEach(function(c) {
|
||||
if (oldChildren[c.key]) {
|
||||
var old = oldChildren[c.key];
|
||||
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||
const old = oldChildren[c.key];
|
||||
const oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||
|
||||
if (oldNode && oldNode.style.left != c.props.style.left) {
|
||||
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
||||
@@ -71,18 +71,18 @@ module.exports = React.createClass({
|
||||
} else {
|
||||
// new element. If we have a startStyle, use that as the style and go through
|
||||
// the enter animations
|
||||
var newProps = {};
|
||||
var restingStyle = c.props.style;
|
||||
const newProps = {};
|
||||
const restingStyle = c.props.style;
|
||||
|
||||
var startStyles = self.props.startStyles;
|
||||
const startStyles = self.props.startStyles;
|
||||
if (startStyles.length > 0) {
|
||||
var startStyle = startStyles[0];
|
||||
const startStyle = startStyles[0];
|
||||
newProps.style = startStyle;
|
||||
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||
}
|
||||
|
||||
newProps.ref = (n => self._collectNode(
|
||||
c.key, n, restingStyle
|
||||
newProps.ref = ((n) => self._collectNode(
|
||||
c.key, n, restingStyle,
|
||||
));
|
||||
|
||||
self.children[c.key] = React.cloneElement(c, newProps);
|
||||
@@ -103,8 +103,8 @@ module.exports = React.createClass({
|
||||
this.nodes[k] === undefined &&
|
||||
this.props.startStyles.length > 0
|
||||
) {
|
||||
var startStyles = this.props.startStyles;
|
||||
var transitionOpts = this.props.enterTransitionOpts;
|
||||
const startStyles = this.props.startStyles;
|
||||
const transitionOpts = this.props.enterTransitionOpts;
|
||||
const domNode = ReactDom.findDOMNode(node);
|
||||
// start from startStyle 1: 0 is the one we gave it
|
||||
// to start with, so now we animate 1 etc.
|
||||
@@ -154,7 +154,7 @@ module.exports = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<span>
|
||||
{Object.values(this.children)}
|
||||
{ Object.values(this.children) }
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
var Velocity = require('velocity-vector');
|
||||
const Velocity = require('velocity-vector');
|
||||
|
||||
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
||||
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
||||
function bounce( p ) {
|
||||
var pow2,
|
||||
let pow2,
|
||||
bounce = 4;
|
||||
|
||||
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
||||
|
||||
@@ -14,13 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
module.exports = {
|
||||
usersTypingApartFromMeAndIgnored: function(room) {
|
||||
return this.usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
|
||||
);
|
||||
},
|
||||
|
||||
usersTypingApartFromMe: function(room) {
|
||||
return this.usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId]
|
||||
room, [MatrixClientPeg.get().credentials.userId],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -29,15 +35,15 @@ module.exports = {
|
||||
* to exclude, return a list of user objects who are typing.
|
||||
*/
|
||||
usersTyping: function(room, exclude) {
|
||||
var whoIsTyping = [];
|
||||
const whoIsTyping = [];
|
||||
|
||||
if (exclude === undefined) {
|
||||
exclude = [];
|
||||
}
|
||||
|
||||
var memberKeys = Object.keys(room.currentState.members);
|
||||
for (var i = 0; i < memberKeys.length; ++i) {
|
||||
var userId = memberKeys[i];
|
||||
const memberKeys = Object.keys(room.currentState.members);
|
||||
for (let i = 0; i < memberKeys.length; ++i) {
|
||||
const userId = memberKeys[i];
|
||||
|
||||
if (room.currentState.members[userId].typing) {
|
||||
if (exclude.indexOf(userId) == -1) {
|
||||
@@ -62,13 +68,11 @@ module.exports = {
|
||||
const names = whoIsTyping.map(function(m) {
|
||||
return m.name;
|
||||
});
|
||||
if (othersCount==1) {
|
||||
return _t('%(names)s and one other are typing', {names: names.slice(0, limit - 1).join(', ')});
|
||||
} else if (othersCount>1) {
|
||||
return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
|
||||
if (othersCount>=1) {
|
||||
return _t('%(names)s and %(count)s others are typing', {names: names.slice(0, limit - 1).join(', '), count: othersCount});
|
||||
} else {
|
||||
const lastPerson = names.pop();
|
||||
return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
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});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
const React = require("react");
|
||||
import { _t } from '../../../languageHandler';
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EncryptedEventDialog',
|
||||
@@ -33,7 +33,7 @@ module.exports = React.createClass({
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
// first try to load the device from our store.
|
||||
//
|
||||
@@ -60,7 +60,7 @@ module.exports = React.createClass({
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
@@ -89,12 +89,12 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_renderDeviceInfo: function() {
|
||||
var device = this.state.device;
|
||||
const device = this.state.device;
|
||||
if (!device) {
|
||||
return (<i>{ _t('unknown device') }</i>);
|
||||
}
|
||||
|
||||
var verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
||||
let verificationStatus = (<b>{ _t('NOT verified') }</b>);
|
||||
if (device.isBlocked()) {
|
||||
verificationStatus = (<b>{ _t('Blacklisted') }</b>);
|
||||
} else if (device.isVerified()) {
|
||||
@@ -118,7 +118,7 @@ module.exports = React.createClass({
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{ _t('Ed25519 fingerprint') }</td>
|
||||
<td><code>{device.getFingerprint()}</code></td>
|
||||
<td><code>{ device.getFingerprint() }</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -126,7 +126,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_renderEventInfo: function() {
|
||||
var event = this.props.event;
|
||||
const event = this.props.event;
|
||||
|
||||
return (
|
||||
<table>
|
||||
@@ -165,36 +165,36 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
|
||||
var buttons = null;
|
||||
let buttons = null;
|
||||
if (this.state.device) {
|
||||
buttons = (
|
||||
<DeviceVerifyButtons device={ this.state.device }
|
||||
userId={ this.props.event.getSender() }
|
||||
<DeviceVerifyButtons device={this.state.device}
|
||||
userId={this.props.event.getSender()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }>
|
||||
<div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{ _t('End-to-end encryption information') }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<h4>{ _t('Event information') }</h4>
|
||||
{this._renderEventInfo()}
|
||||
{ this._renderEventInfo() }
|
||||
|
||||
<h4>{ _t('Sender device information') }</h4>
|
||||
{this._renderDeviceInfo()}
|
||||
{ this._renderDeviceInfo() }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={ this.props.onFinished } autoFocus={ true }>
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onFinished} autoFocus={true}>
|
||||
{ _t('OK') }
|
||||
</button>
|
||||
{buttons}
|
||||
{ buttons }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -136,13 +136,13 @@ export default React.createClass({
|
||||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase1'>
|
||||
{_t("Enter passphrase")}
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
@@ -155,7 +155,7 @@ export default React.createClass({
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase2'>
|
||||
{_t("Confirm passphrase")}
|
||||
{ _t("Confirm passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
@@ -172,7 +172,7 @@ export default React.createClass({
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -134,13 +134,13 @@ export default React.createClass({
|
||||
) }
|
||||
</p>
|
||||
<div className='error'>
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='importFile'>
|
||||
{_t("File to import")}
|
||||
{ _t("File to import") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
@@ -153,14 +153,14 @@ export default React.createClass({
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{_t("Enter passphrase")}
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref='passphrase' id='passphrase'
|
||||
size='64' type='password'
|
||||
onChange={this._onFormChange}
|
||||
disabled={disableForm}/>
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,7 +170,7 @@ export default React.createClass({
|
||||
disabled={!this.state.enableSubmit || disableForm}
|
||||
/>
|
||||
<button onClick={this._onCancelClick} disabled={disableForm}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +29,10 @@ export default class AutocompleteProvider {
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// stub
|
||||
}
|
||||
|
||||
/**
|
||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
import EmojiProvider from './EmojiProvider';
|
||||
import NotifProvider from './NotifProvider';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
export type SelectionRange = {
|
||||
@@ -43,43 +45,59 @@ const PROVIDERS = [
|
||||
UserProvider,
|
||||
RoomProvider,
|
||||
EmojiProvider,
|
||||
NotifProvider,
|
||||
CommandProvider,
|
||||
DuckDuckGoProvider,
|
||||
].map(completer => completer.getInstance());
|
||||
];
|
||||
|
||||
// Providers will get rejected if they take longer than this.
|
||||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||
|
||||
export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||
/* Note: That this waits for all providers to return is *intentional*
|
||||
otherwise, we run into a condition where new completions are displayed
|
||||
while the user is interacting with the list, which makes it difficult
|
||||
to predict whether an action will actually do what is intended
|
||||
*/
|
||||
const completionsList = await Promise.all(
|
||||
// Array of inspections of promises that might timeout. Instead of allowing a
|
||||
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||
// settled, filter for the fulfilled ones
|
||||
PROVIDERS.map((provider) => {
|
||||
return provider
|
||||
.getCompletions(query, selection, force)
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||
.reflect();
|
||||
}),
|
||||
);
|
||||
export default class Autocompleter {
|
||||
constructor(room) {
|
||||
this.room = room;
|
||||
this.providers = PROVIDERS.map((p) => {
|
||||
return new p(room);
|
||||
});
|
||||
}
|
||||
|
||||
return completionsList.filter(
|
||||
(inspection) => inspection.isFulfilled(),
|
||||
).map((completionsState, i) => {
|
||||
return {
|
||||
completions: completionsState.value(),
|
||||
provider: PROVIDERS[i],
|
||||
destroy() {
|
||||
this.providers.forEach((p) => {
|
||||
p.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/* the currently matched "command" the completer tried to complete
|
||||
* we pass this through so that Autocomplete can figure out when to
|
||||
* re-show itself once hidden.
|
||||
*/
|
||||
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
});
|
||||
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||
/* Note: This intentionally waits for all providers to return,
|
||||
otherwise, we run into a condition where new completions are displayed
|
||||
while the user is interacting with the list, which makes it difficult
|
||||
to predict whether an action will actually do what is intended
|
||||
*/
|
||||
const completionsList = await Promise.all(
|
||||
// Array of inspections of promises that might timeout. Instead of allowing a
|
||||
// single timeout to reject the Promise.all, reflect each one and once they've all
|
||||
// settled, filter for the fulfilled ones
|
||||
this.providers.map((provider) => {
|
||||
return provider
|
||||
.getCompletions(query, selection, force)
|
||||
.timeout(PROVIDER_COMPLETION_TIMEOUT)
|
||||
.reflect();
|
||||
}),
|
||||
);
|
||||
|
||||
return completionsList.filter(
|
||||
(inspection) => inspection.isFulfilled(),
|
||||
).map((completionsState, i) => {
|
||||
return {
|
||||
completions: completionsState.value(),
|
||||
provider: this.providers[i],
|
||||
|
||||
/* the currently matched "command" the completer tried to complete
|
||||
* we pass this through so that Autocomplete can figure out when to
|
||||
* re-show itself once hidden.
|
||||
*/
|
||||
command: this.providers[i].getCurrentCommand(query, selection, force),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {TextualCompletion} from './Components';
|
||||
@@ -27,80 +28,88 @@ const COMMANDS = [
|
||||
{
|
||||
command: '/me',
|
||||
args: '<message>',
|
||||
description: 'Displays action',
|
||||
description: _td('Displays action'),
|
||||
},
|
||||
{
|
||||
command: '/ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Bans user with given id',
|
||||
description: _td('Bans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/unban',
|
||||
args: '<user-id>',
|
||||
description: 'Unbans user with given id',
|
||||
description: _td('Unbans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/op',
|
||||
args: '<user-id> [<power-level>]',
|
||||
description: 'Define the power level of a user',
|
||||
description: _td('Define the power level of a user'),
|
||||
},
|
||||
{
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
description: 'Deops user with given id',
|
||||
description: _td('Deops user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: 'Invites user with given id to current room',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
args: '<room-alias>',
|
||||
description: 'Joins room with given alias',
|
||||
description: _td('Joins room with given alias'),
|
||||
},
|
||||
{
|
||||
command: '/part',
|
||||
args: '[<room-alias>]',
|
||||
description: 'Leave room',
|
||||
description: _td('Leave room'),
|
||||
},
|
||||
{
|
||||
command: '/topic',
|
||||
args: '<topic>',
|
||||
description: 'Sets the room topic',
|
||||
description: _td('Sets the room topic'),
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Kicks user with given id',
|
||||
description: _td('Kicks user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/nick',
|
||||
args: '<display-name>',
|
||||
description: 'Changes your display nickname',
|
||||
description: _td('Changes your display nickname'),
|
||||
},
|
||||
{
|
||||
command: '/ddg',
|
||||
args: '<query>',
|
||||
description: 'Searches DuckDuckGo for results',
|
||||
description: _td('Searches DuckDuckGo for results'),
|
||||
},
|
||||
{
|
||||
command: '/tint',
|
||||
args: '<color1> [<color2>]',
|
||||
description: 'Changes colour scheme of current room',
|
||||
description: _td('Changes colour scheme of current room'),
|
||||
},
|
||||
{
|
||||
command: '/verify',
|
||||
args: '<user-id> <device-id> <device-signing-key>',
|
||||
description: 'Verifies a user, device, and pubkey tuple',
|
||||
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||
},
|
||||
{
|
||||
command: '/ignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Ignores a user, hiding their messages from you'),
|
||||
},
|
||||
{
|
||||
command: '/unignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||
},
|
||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||
];
|
||||
|
||||
const COMMAND_RE = /(^\/\w*)/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class CommandProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(COMMAND_RE);
|
||||
@@ -119,7 +128,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||
component: (<TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={ _t(result.description) }
|
||||
description={_t(result.description)}
|
||||
/>),
|
||||
range,
|
||||
};
|
||||
@@ -132,15 +141,9 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||
return '*️⃣ ' + _t('Commands');
|
||||
}
|
||||
|
||||
static getInstance(): CommandProvider {
|
||||
if (instance === null) instance = new CommandProvider();
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_block">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@ export class TextualCompletion extends React.Component {
|
||||
subtitle,
|
||||
description,
|
||||
className,
|
||||
...restProps,
|
||||
...restProps
|
||||
} = this.props;
|
||||
return (
|
||||
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
|
||||
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -56,14 +56,14 @@ export class PillCompletion extends React.Component {
|
||||
description,
|
||||
initialComponent,
|
||||
className,
|
||||
...restProps,
|
||||
...restProps
|
||||
} = this.props;
|
||||
return (
|
||||
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
||||
{initialComponent}
|
||||
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||
{ initialComponent }
|
||||
<span className="mx_Autocomplete_Completion_title">{ title }</span>
|
||||
<span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
|
||||
<span className="mx_Autocomplete_Completion_description">{ description }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -25,8 +26,6 @@ import {TextualCompletion} from './Components';
|
||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||
const REFERRER = 'vector';
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(DDG_REGEX);
|
||||
@@ -38,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (!query || !command) {
|
||||
return [];
|
||||
}
|
||||
@@ -47,7 +46,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
let results = json.Results.map(result => {
|
||||
const results = json.Results.map((result) => {
|
||||
return {
|
||||
completion: result.Text,
|
||||
component: (
|
||||
@@ -96,16 +95,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||
return '🔍 ' + _t('Results from DuckDuckGo');
|
||||
}
|
||||
|
||||
static getInstance(): DuckDuckGoProvider {
|
||||
if (instance == null) {
|
||||
instance = new DuckDuckGoProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_block">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +26,7 @@ import {PillCompletion} from './Components';
|
||||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
import EmojiData from '../stripped-emoji.json';
|
||||
|
||||
@@ -69,8 +71,6 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor
|
||||
};
|
||||
});
|
||||
|
||||
let instance = null;
|
||||
|
||||
function score(query, space) {
|
||||
const index = space.indexOf(query);
|
||||
if (index === -1) {
|
||||
@@ -96,6 +96,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
|
||||
return []; // don't give any suggestions if the user doesn't want them
|
||||
}
|
||||
|
||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||
|
||||
let completions = [];
|
||||
@@ -133,7 +137,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
return {
|
||||
completion: unicode,
|
||||
component: (
|
||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
|
||||
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
@@ -146,15 +150,9 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
return '😃 ' + _t('Emoji');
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (instance == null)
|
||||
{instance = new EmojiProvider();}
|
||||
return instance;
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
62
src/autocomplete/NotifProvider.js
Normal file
62
src/autocomplete/NotifProvider.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 AutocompleteProvider from './AutocompleteProvider';
|
||||
import { _t } from '../languageHandler';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import {PillCompletion} from './Components';
|
||||
import sdk from '../index';
|
||||
|
||||
const AT_ROOM_REGEX = /@\S*/g;
|
||||
|
||||
export default class NotifProvider extends AutocompleteProvider {
|
||||
constructor(room) {
|
||||
super(AT_ROOM_REGEX);
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];
|
||||
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) {
|
||||
return [{
|
||||
completion: '@room',
|
||||
suffix: ' ',
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
|
||||
),
|
||||
range,
|
||||
}];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getName() {
|
||||
return '❗️ ' + _t('Room Notification');
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -27,8 +28,6 @@ import _sortBy from 'lodash/sortBy';
|
||||
|
||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
function score(query, space) {
|
||||
const index = space.indexOf(query);
|
||||
if (index === -1) {
|
||||
@@ -96,17 +95,9 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
return '💬 ' + _t('Rooms');
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new RoomProvider();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -30,19 +31,57 @@ import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||
|
||||
const USER_REGEX = /@\S*/g;
|
||||
|
||||
let instance = null;
|
||||
|
||||
export default class UserProvider extends AutocompleteProvider {
|
||||
users: Array<RoomMember> = [];
|
||||
users: Array<RoomMember> = null;
|
||||
room: Room = null;
|
||||
|
||||
constructor() {
|
||||
constructor(room) {
|
||||
super(USER_REGEX, {
|
||||
keys: ['name'],
|
||||
});
|
||||
this.room = room;
|
||||
this.matcher = new FuzzyMatcher([], {
|
||||
keys: ['name', 'userId'],
|
||||
shouldMatchPrefix: true,
|
||||
});
|
||||
|
||||
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
||||
this._onRoomStateMemberBound = this._onRoomStateMember.bind(this);
|
||||
|
||||
MatrixClientPeg.get().on("Room.timeline", this._onRoomTimelineBound);
|
||||
MatrixClientPeg.get().on("RoomState.members", this._onRoomStateMemberBound);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this._onRoomTimelineBound);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this._onRoomStateMemberBound);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||
if (!room) return;
|
||||
if (removed) return;
|
||||
if (room.roomId !== this.room.roomId) return;
|
||||
|
||||
// ignore events from filtered timelines
|
||||
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
|
||||
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
// updates from pagination will happen when the paginate completes.
|
||||
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||
|
||||
this.onUserSpoke(ev.sender);
|
||||
}
|
||||
|
||||
_onRoomStateMember(ev, state, member) {
|
||||
// ignore members in other rooms
|
||||
if (member.roomId !== this.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// blow away the users cache
|
||||
this.users = null;
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||
@@ -54,8 +93,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
return [];
|
||||
}
|
||||
|
||||
// lazy-load user list into matcher
|
||||
if (this.users === null) this._makeUsers();
|
||||
|
||||
let completions = [];
|
||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
completions = this.matcher.match(command[0]).map((user) => {
|
||||
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||
@@ -67,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
href: 'https://matrix.to/#/' + user.userId,
|
||||
component: (
|
||||
<PillCompletion
|
||||
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
|
||||
title={displayName}
|
||||
description={user.userId} />
|
||||
),
|
||||
@@ -82,16 +124,16 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
return '👥 ' + _t('Users');
|
||||
}
|
||||
|
||||
setUserListFromRoom(room: Room) {
|
||||
const events = room.getLiveTimeline().getEvents();
|
||||
_makeUsers() {
|
||||
const events = this.room.getLiveTimeline().getEvents();
|
||||
const lastSpoken = {};
|
||||
|
||||
for(const event of events) {
|
||||
for (const event of events) {
|
||||
lastSpoken[event.getSender()] = event.getTs();
|
||||
}
|
||||
|
||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||
this.users = room.getJoinedMembers().filter((member) => {
|
||||
this.users = this.room.getJoinedMembers().filter((member) => {
|
||||
if (member.userId !== currentUserId) return true;
|
||||
});
|
||||
|
||||
@@ -103,7 +145,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
onUserSpoke(user: RoomMember) {
|
||||
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||
if (this.users === null) return;
|
||||
if (user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||
|
||||
// Move the user that spoke to the front of the array
|
||||
this.users.splice(
|
||||
@@ -113,16 +156,9 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
this.matcher.setObjects(this.users);
|
||||
}
|
||||
|
||||
static getInstance(): UserProvider {
|
||||
if (instance == null) {
|
||||
instance = new UserProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{completions}
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var classNames = require('classnames');
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
@@ -33,10 +33,11 @@ module.exports = {
|
||||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
menuColour: React.PropTypes.string,
|
||||
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
||||
},
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
var container = document.getElementById(this.ContextualMenuContainerId);
|
||||
let container = document.getElementById(this.ContextualMenuContainerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
@@ -48,9 +49,9 @@ module.exports = {
|
||||
},
|
||||
|
||||
createMenu: function(Element, props) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
var closeMenu = function() {
|
||||
const closeMenu = function() {
|
||||
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||
|
||||
if (props && props.onFinished) {
|
||||
@@ -58,47 +59,64 @@ module.exports = {
|
||||
}
|
||||
};
|
||||
|
||||
var position = {
|
||||
top: props.top,
|
||||
};
|
||||
const position = {};
|
||||
let chevronFace = null;
|
||||
|
||||
var chevronOffset = {};
|
||||
if (props.chevronOffset) {
|
||||
if (props.top) {
|
||||
position.top = props.top;
|
||||
} else {
|
||||
position.bottom = props.bottom;
|
||||
}
|
||||
|
||||
if (props.left) {
|
||||
position.left = props.left;
|
||||
chevronFace = 'left';
|
||||
} else {
|
||||
position.right = props.right;
|
||||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
}
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
chevronOffset.top = props.chevronOffset;
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
var chevronCSS = "";
|
||||
let chevronCSS = "";
|
||||
if (props.menuColour) {
|
||||
chevronCSS = `
|
||||
.mx_ContextualMenu_chevron_left:after {
|
||||
border-right-color: ${props.menuColour};
|
||||
}
|
||||
|
||||
.mx_ContextualMenu_chevron_right:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_top:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
.mx_ContextualMenu_chevron_bottom:after {
|
||||
border-left-color: ${props.menuColour};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
var chevron = null;
|
||||
if (props.left) {
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
|
||||
position.left = props.left;
|
||||
} else {
|
||||
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
|
||||
position.right = props.right;
|
||||
}
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
var className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
var menuClasses = classNames({
|
||||
const menuClasses = classNames({
|
||||
'mx_ContextualMenu': true,
|
||||
'mx_ContextualMenu_left': props.left,
|
||||
'mx_ContextualMenu_right': !props.left,
|
||||
'mx_ContextualMenu_left': chevronFace === 'left',
|
||||
'mx_ContextualMenu_right': chevronFace === 'right',
|
||||
'mx_ContextualMenu_top': chevronFace === 'top',
|
||||
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
|
||||
});
|
||||
|
||||
var menuStyle = {};
|
||||
const menuStyle = {};
|
||||
if (props.menuWidth) {
|
||||
menuStyle.width = props.menuWidth;
|
||||
}
|
||||
@@ -113,14 +131,14 @@ module.exports = {
|
||||
|
||||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
var menu = (
|
||||
const menu = (
|
||||
<div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle}>
|
||||
{chevron}
|
||||
<Element {...props} onFinished={closeMenu}/>
|
||||
{ chevron }
|
||||
<Element {...props} onFinished={closeMenu} />
|
||||
</div>
|
||||
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||
<style>{chevronCSS}</style>
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onCreateRoom: function() {
|
||||
var options = {};
|
||||
const options = {};
|
||||
|
||||
if (this.state.room_name) {
|
||||
options.name = this.state.room_name;
|
||||
@@ -79,14 +79,14 @@ module.exports = React.createClass({
|
||||
{
|
||||
type: "m.room.join_rules",
|
||||
content: {
|
||||
"join_rule": this.state.is_private ? "invite" : "public"
|
||||
}
|
||||
"join_rule": this.state.is_private ? "invite" : "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
content: {
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited"
|
||||
}
|
||||
"history_visibility": this.state.share_history ? "shared" : "invited",
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -94,19 +94,19 @@ module.exports = React.createClass({
|
||||
|
||||
options.invite = this.state.invited_users;
|
||||
|
||||
var alias = this.getAliasLocalpart();
|
||||
const alias = this.getAliasLocalpart();
|
||||
if (alias) {
|
||||
options.room_alias_name = alias;
|
||||
}
|
||||
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
// TODO: Error.
|
||||
console.error("Cannot create room: No matrix client.");
|
||||
return;
|
||||
}
|
||||
|
||||
var deferred = cli.createRoom(options);
|
||||
const deferred = cli.createRoom(options);
|
||||
|
||||
if (this.state.encrypt) {
|
||||
// TODO
|
||||
@@ -116,7 +116,7 @@ module.exports = React.createClass({
|
||||
phase: this.phases.CREATING,
|
||||
});
|
||||
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
deferred.then(function(resp) {
|
||||
self.setState({
|
||||
@@ -209,7 +209,7 @@ module.exports = React.createClass({
|
||||
|
||||
onAliasChanged: function(alias) {
|
||||
this.setState({
|
||||
alias: alias
|
||||
alias: alias,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -220,64 +220,64 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var curr_phase = this.state.phase;
|
||||
const curr_phase = this.state.phase;
|
||||
if (curr_phase == this.phases.CREATING) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader/>
|
||||
<Loader />
|
||||
);
|
||||
} else {
|
||||
var error_box = "";
|
||||
let error_box = "";
|
||||
if (curr_phase == this.phases.ERROR) {
|
||||
error_box = (
|
||||
<div className="mx_Error">
|
||||
{_t('An error occurred: %(error_string)s', {error_string: this.state.error_string})}
|
||||
{ _t('An error occurred: %(error_string)s', {error_string: this.state.error_string}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||
var Presets = sdk.getComponent("create_room.Presets");
|
||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
var SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
||||
const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||
const RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||
const Presets = sdk.getComponent("create_room.Presets");
|
||||
const UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
|
||||
|
||||
var domain = MatrixClientPeg.get().getDomain();
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
return (
|
||||
<div className="mx_CreateRoom">
|
||||
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={ this.props.collapsedRhs }/>
|
||||
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={this.props.collapsedRhs} />
|
||||
<div className="mx_CreateRoom_body">
|
||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <br />
|
||||
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')}/> <br />
|
||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
|
||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')} /> <br />
|
||||
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')} /> <br />
|
||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={domain} onChange={this.onAliasChanged} /> <br />
|
||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged} /> <br />
|
||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset} /> <br />
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
||||
{_t('Make this room private')}
|
||||
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged} />
|
||||
{ _t('Make this room private') }
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
||||
{_t('Share message history with new users')}
|
||||
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged} />
|
||||
{ _t('Share message history with new users') }
|
||||
</label>
|
||||
</div>
|
||||
<div className="mx_CreateRoom_encrypt">
|
||||
<label>
|
||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
||||
{_t('Encrypt room')}
|
||||
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged} />
|
||||
{ _t('Encrypt room') }
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
||||
</div>
|
||||
{error_box}
|
||||
{ error_box }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,12 +19,12 @@ import React from 'react';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
/*
|
||||
* Component which shows the filtered file using a TimelinePanel
|
||||
*/
|
||||
var FilePanel = React.createClass({
|
||||
const FilePanel = React.createClass({
|
||||
displayName: 'FilePanel',
|
||||
|
||||
propTypes: {
|
||||
@@ -55,33 +55,33 @@ var FilePanel = React.createClass({
|
||||
},
|
||||
|
||||
updateTimelineSet: function(roomId) {
|
||||
var client = MatrixClientPeg.get();
|
||||
var room = client.getRoom(roomId);
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(roomId);
|
||||
|
||||
this.noRoom = !room;
|
||||
|
||||
if (room) {
|
||||
var filter = new Matrix.Filter(client.credentials.userId);
|
||||
const filter = new Matrix.Filter(client.credentials.userId);
|
||||
filter.setDefinition(
|
||||
{
|
||||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true
|
||||
"contains_url": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// FIXME: we shouldn't be doing this every time we change room - see comment above.
|
||||
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
|
||||
(filterId)=>{
|
||||
filter.filterId = filterId;
|
||||
var timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||
const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
|
||||
this.setState({ timelineSet: timelineSet });
|
||||
},
|
||||
(error)=>{
|
||||
console.error("Failed to get or create file panel filter", error);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
console.error("Failed to add filtered timelineSet for FilePanel as no room!");
|
||||
@@ -92,18 +92,21 @@ var FilePanel = React.createClass({
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">
|
||||
{_tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{sub}</a>)}
|
||||
{ _t("You must <a>register</a> to use this functionality",
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
} else if (this.noRoom) {
|
||||
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
|
||||
<div className="mx_RoomView_empty">{_t("You must join the room to see its files")}</div>
|
||||
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
if (this.state.timelineSet) {
|
||||
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
|
||||
@@ -114,17 +117,15 @@ var FilePanel = React.createClass({
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={this.state.timelineSet}
|
||||
showUrlPreview = { false }
|
||||
showUrlPreview = {false}
|
||||
tileShape="file_grid"
|
||||
opacity={ this.props.opacity }
|
||||
empty={_t('There are no visible files in this room')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_FilePanel">
|
||||
<Loader/>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,7 +107,7 @@ export default React.createClass({
|
||||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
errorText: msg,
|
||||
});
|
||||
}).done();
|
||||
|
||||
@@ -207,7 +207,7 @@ export default React.createClass({
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -215,8 +215,8 @@ export default React.createClass({
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
{ this._renderCurrentStage() }
|
||||
{ error }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,9 +18,10 @@ limitations under the License.
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import KeyCode from '../../KeyCode';
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
@@ -27,6 +29,7 @@ import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
@@ -37,7 +40,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
*
|
||||
* Components mounted below us can access the matrix client via the react context.
|
||||
*/
|
||||
export default React.createClass({
|
||||
const LoggedInView = React.createClass({
|
||||
displayName: 'LoggedInView',
|
||||
|
||||
propTypes: {
|
||||
@@ -73,7 +76,7 @@ export default React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'),
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -81,10 +84,6 @@ export default React.createClass({
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
||||
// RoomView.getScrollState()
|
||||
this._scrollStateMap = {};
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
@@ -116,10 +115,6 @@ export default React.createClass({
|
||||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
getScrollStateForRoom: function(roomId) {
|
||||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
@@ -139,6 +134,9 @@ export default React.createClass({
|
||||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({action: "ignore_state_changed"});
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
@@ -157,19 +155,13 @@ export default React.createClass({
|
||||
*/
|
||||
|
||||
let handled = false;
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
let ctrlCmdOnly;
|
||||
if (isMac) {
|
||||
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||
} else {
|
||||
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
let action = ev.keyCode == KeyCode.UP ?
|
||||
const action = ev.keyCode == KeyCode.UP ?
|
||||
'view_prev_room' : 'view_next_room';
|
||||
dis.dispatch({action: action});
|
||||
handled = true;
|
||||
@@ -211,13 +203,13 @@ export default React.createClass({
|
||||
_onScrollKeyPressed: function(ev) {
|
||||
if (this.refs.roomView) {
|
||||
this.refs.roomView.handleScrollKey(ev);
|
||||
}
|
||||
else if (this.refs.roomDirectory) {
|
||||
} else if (this.refs.roomDirectory) {
|
||||
this.refs.roomDirectory.handleScrollKey(ev);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
@@ -245,23 +237,23 @@ export default React.createClass({
|
||||
oobData={this.props.roomOobData}
|
||||
eventPixelOffset={this.props.initialEventPixelOffset}
|
||||
key={this.props.currentRoomId || 'roomview'}
|
||||
opacity={this.props.middleOpacity}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
disabled={this.props.middleDisabled}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
ConferenceHandler={this.props.ConferenceHandler}
|
||||
scrollStateMap={this._scrollStateMap}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) {
|
||||
right_panel = <RightPanel roomId={this.props.currentRoomId} disabled={this.props.rightDisabled} />;
|
||||
}
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
page_element = <UserSettings
|
||||
onClose={this.props.onUserSettingsClose}
|
||||
brand={this.props.config.brand}
|
||||
enableLabs={this.props.config.enableLabs}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
@@ -271,9 +263,9 @@ export default React.createClass({
|
||||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
onRoomCreated={this.props.onRoomCreated}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
|
||||
case PageTypes.RoomDirectory:
|
||||
@@ -301,13 +293,15 @@ export default React.createClass({
|
||||
|
||||
case PageTypes.UserView:
|
||||
page_element = null; // deliberately null for now
|
||||
right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||
right_panel = <RightPanel disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
case PageTypes.GroupView:
|
||||
page_element = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
isNew={this.props.currentGroupIsNew}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -325,7 +319,7 @@ export default React.createClass({
|
||||
topBar = <MatrixToolbar />;
|
||||
}
|
||||
|
||||
var bodyClasses = 'mx_MatrixChat';
|
||||
let bodyClasses = 'mx_MatrixChat';
|
||||
if (topBar) {
|
||||
bodyClasses += ' mx_MatrixChat_toolbarShowing';
|
||||
}
|
||||
@@ -335,19 +329,22 @@ export default React.createClass({
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
{topBar}
|
||||
{ topBar }
|
||||
<div className={bodyClasses}>
|
||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
opacity={this.props.leftOpacity}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{page_element}
|
||||
{ page_element }
|
||||
</main>
|
||||
{right_panel}
|
||||
{ right_panel }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default DragDropContext(HTML5Backend)(LoggedInView);
|
||||
|
||||
@@ -22,7 +22,6 @@ import React from 'react';
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
@@ -32,19 +31,18 @@ import dis from "../../dispatcher";
|
||||
import Modal from "../../Modal";
|
||||
import Tinter from "../../Tinter";
|
||||
import sdk from '../../index';
|
||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
|
||||
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import linkifyMatrix from "../../linkify-matrix";
|
||||
import * as Lifecycle from '../../Lifecycle';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
require('../../stores/LifecycleStore');
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
import * as UDEHandler from '../../UnknownDeviceErrorHandler';
|
||||
import KeyRequestHandler from '../../KeyRequestHandler';
|
||||
import { _t, getCurrentLanguage } from '../../languageHandler';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
const VIEWS = {
|
||||
@@ -75,7 +73,17 @@ const VIEWS = {
|
||||
LOGGED_IN: 6,
|
||||
};
|
||||
|
||||
module.exports = React.createClass({
|
||||
// Actions that are redirected through the onboarding process prior to being
|
||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||
// re-factoring to be included in this list in future.
|
||||
const ONBOARDING_FLOW_STARTERS = [
|
||||
'view_user_settings',
|
||||
'view_create_chat',
|
||||
'view_create_room',
|
||||
'view_create_group',
|
||||
];
|
||||
|
||||
export default React.createClass({
|
||||
// we export this so that the integration tests can use it :-S
|
||||
statics: {
|
||||
VIEWS: VIEWS,
|
||||
@@ -144,11 +152,11 @@ module.exports = React.createClass({
|
||||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||
viewUserId: null,
|
||||
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
leftOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
rightOpacity: 1.0,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
leftDisabled: false,
|
||||
middleDisabled: false,
|
||||
rightDisabled: false,
|
||||
|
||||
version: null,
|
||||
newVersion: null,
|
||||
@@ -214,10 +222,7 @@ module.exports = React.createClass({
|
||||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
||||
this._onRoomViewStoreUpdated();
|
||||
|
||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||
if (!SettingsStore.getValue("analyticsOptOut")) Analytics.enable();
|
||||
|
||||
// Used by _viewRoom before getting state from sync
|
||||
this.firstSyncComplete = false;
|
||||
@@ -280,11 +285,15 @@ module.exports = React.createClass({
|
||||
this._windowWidth = 10000;
|
||||
this.handleResize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
|
||||
// check we have the right tint applied for this theme.
|
||||
// N.B. we don't call the whole of setTheme() here as we may be
|
||||
// racing with the theme CSS download finishing from index.js
|
||||
Tinter.tint();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
UDEHandler.startListening();
|
||||
|
||||
this.focusComposer = false;
|
||||
|
||||
@@ -305,7 +314,7 @@ module.exports = React.createClass({
|
||||
|
||||
// the first thing to do is to try the token params in the query-string
|
||||
Lifecycle.attemptTokenLogin(this.props.realQueryParams).then((loggedIn) => {
|
||||
if(loggedIn) {
|
||||
if (loggedIn) {
|
||||
this.props.onTokenLoginCompleted();
|
||||
|
||||
// don't do anything else until the page reloads - just stay in
|
||||
@@ -350,10 +359,8 @@ module.exports = React.createClass({
|
||||
componentWillUnmount: function() {
|
||||
Lifecycle.stopMatrixClient();
|
||||
dis.unregister(this.dispatcherRef);
|
||||
UDEHandler.stopListening();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this._roomViewStoreToken.remove();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
@@ -379,6 +386,22 @@ module.exports = React.createClass({
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// Start the onboarding process for certain actions
|
||||
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest() &&
|
||||
ONBOARDING_FLOW_STARTERS.includes(payload.action)
|
||||
) {
|
||||
// This will cause `payload` to be dispatched later, once a
|
||||
// sync has reached the "prepared" state. Setting a matrix ID
|
||||
// will cause a full login and sync and finally the deferred
|
||||
// action will be dispatched.
|
||||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: payload,
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (payload.action) {
|
||||
case 'logout':
|
||||
Lifecycle.logout();
|
||||
@@ -439,7 +462,7 @@ module.exports = React.createClass({
|
||||
break;
|
||||
case 'view_user':
|
||||
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
|
||||
if (this.state.collapse_rhs) {
|
||||
if (this.state.collapseRhs) {
|
||||
setTimeout(()=>{
|
||||
dis.dispatch({
|
||||
action: 'show_right_panel',
|
||||
@@ -468,22 +491,17 @@ module.exports = React.createClass({
|
||||
this._viewIndexedRoom(payload.roomIndex);
|
||||
break;
|
||||
case 'view_user_settings':
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'view_user_settings',
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
break;
|
||||
}
|
||||
this._setPage(PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this._createRoom();
|
||||
break;
|
||||
case 'view_create_group': {
|
||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
|
||||
}
|
||||
break;
|
||||
case 'view_room_directory':
|
||||
this._setPage(PageTypes.RoomDirectory);
|
||||
this.notifyNewScreen('directory');
|
||||
@@ -495,7 +513,10 @@ module.exports = React.createClass({
|
||||
case 'view_group':
|
||||
{
|
||||
const groupId = payload.group_id;
|
||||
this.setState({currentGroupId: groupId});
|
||||
this.setState({
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
@@ -511,7 +532,7 @@ module.exports = React.createClass({
|
||||
this._chatCreateOrReuse(payload.user_id, payload.go_home_on_cancel);
|
||||
break;
|
||||
case 'view_create_chat':
|
||||
this._createChat();
|
||||
showStartChatInviteDialog();
|
||||
break;
|
||||
case 'view_invite':
|
||||
showRoomInviteDialog(payload.roomId);
|
||||
@@ -521,30 +542,29 @@ module.exports = React.createClass({
|
||||
break;
|
||||
case 'hide_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: true,
|
||||
collapseLhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_left_panel':
|
||||
this.setState({
|
||||
collapse_lhs: false,
|
||||
collapseLhs: false,
|
||||
});
|
||||
break;
|
||||
case 'hide_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: true,
|
||||
collapseRhs: true,
|
||||
});
|
||||
break;
|
||||
case 'show_right_panel':
|
||||
this.setState({
|
||||
collapse_rhs: false,
|
||||
collapseRhs: false,
|
||||
});
|
||||
break;
|
||||
case 'ui_opacity': {
|
||||
const sideDefault = payload.sideOpacity >= 0.0 ? payload.sideOpacity : 1.0;
|
||||
case 'panel_disable': {
|
||||
this.setState({
|
||||
leftOpacity: payload.leftOpacity >= 0.0 ? payload.leftOpacity : sideDefault,
|
||||
middleOpacity: payload.middleOpacity || 1.0,
|
||||
rightOpacity: payload.rightOpacity >= 0.0 ? payload.rightOpacity : sideDefault,
|
||||
leftDisabled: payload.leftDisabled || payload.sideDisabled || false,
|
||||
middleDisabled: payload.middleDisabled || false,
|
||||
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
||||
});
|
||||
break; }
|
||||
case 'set_theme':
|
||||
@@ -572,6 +592,9 @@ module.exports = React.createClass({
|
||||
this._onWillStartClient();
|
||||
});
|
||||
break;
|
||||
case 'client_started':
|
||||
this._onClientStarted();
|
||||
break;
|
||||
case 'new_version':
|
||||
this.onVersion(
|
||||
payload.currentVersion, payload.newVersion,
|
||||
@@ -587,10 +610,6 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdated: function() {
|
||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
||||
},
|
||||
|
||||
_setPage: function(pageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
@@ -677,10 +696,10 @@ module.exports = React.createClass({
|
||||
this.focusComposer = true;
|
||||
|
||||
const newState = {
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
autoJoin: roomInfo.auto_join,
|
||||
};
|
||||
|
||||
if (roomInfo.room_alias) {
|
||||
@@ -757,40 +776,14 @@ module.exports = React.createClass({
|
||||
}).close;
|
||||
},
|
||||
|
||||
_createChat: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'view_create_chat',
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
showStartChatInviteDialog();
|
||||
},
|
||||
|
||||
_createRoom: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({
|
||||
action: 'do_after_sync_prepared',
|
||||
deferred_action: {
|
||||
action: 'view_create_room',
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
}
|
||||
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog");
|
||||
Modal.createTrackedDialog('Create Room', '', TextInputDialog, {
|
||||
title: _t('Create Room'),
|
||||
description: _t('Room name (optional)'),
|
||||
button: _t('Create Room'),
|
||||
onFinished: (shouldCreate, name) => {
|
||||
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
|
||||
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||
onFinished: (shouldCreate, name, noFederate) => {
|
||||
if (shouldCreate) {
|
||||
const createOpts = {};
|
||||
if (name) createOpts.name = name;
|
||||
if (noFederate) createOpts.creation_content = {'m.federate': false};
|
||||
createRoom({createOpts}).done();
|
||||
}
|
||||
},
|
||||
@@ -862,7 +855,7 @@ module.exports = React.createClass({
|
||||
title: _t("Leave room"),
|
||||
description: (
|
||||
<span>
|
||||
{_t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name})}
|
||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
</span>
|
||||
),
|
||||
onFinished: (shouldLeave) => {
|
||||
@@ -899,7 +892,7 @@ module.exports = React.createClass({
|
||||
*/
|
||||
_onSetTheme: function(theme) {
|
||||
if (!theme) {
|
||||
theme = 'light';
|
||||
theme = SettingsStore.getValue("theme");
|
||||
}
|
||||
|
||||
// look for the stylesheet elements.
|
||||
@@ -922,18 +915,49 @@ module.exports = React.createClass({
|
||||
// disable all of them first, then enable the one we want. Chrome only
|
||||
// bothers to do an update on a true->false transition, so this ensures
|
||||
// that we get exactly one update, at the right time.
|
||||
//
|
||||
// ^ This comment was true when we used to use alternative stylesheets
|
||||
// for the CSS. Nowadays we just set them all as disabled in index.html
|
||||
// and enable them as needed. It might be cleaner to disable them all
|
||||
// at the same time to prevent loading two themes simultaneously and
|
||||
// having them interact badly... but this causes a flash of unstyled app
|
||||
// which is even uglier. So we don't.
|
||||
|
||||
Object.values(styleElements).forEach((a) => {
|
||||
a.disabled = true;
|
||||
});
|
||||
styleElements[theme].disabled = false;
|
||||
|
||||
if (theme === 'dark') {
|
||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
||||
// XXX: obviously this shouldn't be hardcoded here.
|
||||
Tinter.tintSvgWhite('#2d2d2d');
|
||||
} else {
|
||||
Tinter.tintSvgWhite('#ffffff');
|
||||
const switchTheme = function() {
|
||||
// we re-enable our theme here just in case we raced with another
|
||||
// theme set request as per https://github.com/vector-im/riot-web/issues/5601.
|
||||
// We could alternatively lock or similar to stop the race, but
|
||||
// this is probably good enough for now.
|
||||
styleElements[theme].disabled = false;
|
||||
Object.values(styleElements).forEach((a) => {
|
||||
if (a == styleElements[theme]) return;
|
||||
a.disabled = true;
|
||||
});
|
||||
Tinter.setTheme(theme);
|
||||
};
|
||||
|
||||
// turns out that Firefox preloads the CSS for link elements with
|
||||
// the disabled attribute, but Chrome doesn't.
|
||||
|
||||
let cssLoaded = false;
|
||||
|
||||
styleElements[theme].onload = () => {
|
||||
switchTheme();
|
||||
};
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (ss && ss.href === styleElements[theme].href) {
|
||||
cssLoaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cssLoaded) {
|
||||
styleElements[theme].onload = undefined;
|
||||
switchTheme();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1002,8 +1026,8 @@ module.exports = React.createClass({
|
||||
this.setStateForNewView({
|
||||
view: VIEWS.LOGIN,
|
||||
ready: false,
|
||||
collapse_lhs: false,
|
||||
collapse_rhs: false,
|
||||
collapseLhs: false,
|
||||
collapseRhs: false,
|
||||
currentRoomId: null,
|
||||
page_type: PageTypes.RoomDirectory,
|
||||
});
|
||||
@@ -1041,10 +1065,10 @@ module.exports = React.createClass({
|
||||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self.refs.loggedInView) {
|
||||
if (!self._loggedInView) {
|
||||
return true;
|
||||
}
|
||||
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||
return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
@@ -1104,6 +1128,65 @@ module.exports = React.createClass({
|
||||
cli.on("crypto.roomKeyRequestCancellation", (req) => {
|
||||
krh.handleKeyRequestCancellation(req);
|
||||
});
|
||||
cli.on("Room", (room) => {
|
||||
if (MatrixClientPeg.get().isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
SettingLevel.ROOM_DEVICE,
|
||||
"blacklistUnverifiedDevices",
|
||||
room.roomId,
|
||||
/*explicit=*/true,
|
||||
);
|
||||
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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Called shortly after the matrix client has started. Useful for
|
||||
* setting up anything that requires the client to be started.
|
||||
* @private
|
||||
*/
|
||||
_onClientStarted: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (cli.isCryptoEnabled()) {
|
||||
const blacklistEnabled = SettingsStore.getValueAt(
|
||||
SettingLevel.DEVICE,
|
||||
"blacklistUnverifiedDevices",
|
||||
);
|
||||
cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
|
||||
}
|
||||
},
|
||||
|
||||
showScreen: function(screen, params) {
|
||||
@@ -1343,13 +1426,6 @@ module.exports = React.createClass({
|
||||
cli.sendEvent(roomId, event.getType(), event.getContent()).done(() => {
|
||||
dis.dispatch({action: 'message_sent'});
|
||||
}, (err) => {
|
||||
if (err.name === 'UnknownDeviceError') {
|
||||
dis.dispatch({
|
||||
action: 'unknown_device_error',
|
||||
err: err,
|
||||
room: cli.getRoom(roomId),
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: 'message_send_failed'});
|
||||
});
|
||||
},
|
||||
@@ -1411,6 +1487,10 @@ module.exports = React.createClass({
|
||||
return this.props.makeRegistrationUrl(params);
|
||||
},
|
||||
|
||||
_collectLoggedInView: function(ref) {
|
||||
this._loggedInView = ref;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
@@ -1443,7 +1523,7 @@ module.exports = React.createClass({
|
||||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onRegistered={this.onRegistered}
|
||||
@@ -1459,7 +1539,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
@@ -65,7 +65,7 @@ module.exports = React.createClass({
|
||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
|
||||
// whether to show read receipts
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
|
||||
// true if updates to the event list should cause the scroll panel to
|
||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||
@@ -78,9 +78,6 @@ module.exports = React.createClass({
|
||||
// callback which is called when more content is needed.
|
||||
onFillRequest: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// className for the panel
|
||||
className: React.PropTypes.string.isRequired,
|
||||
|
||||
@@ -112,8 +109,6 @@ module.exports = React.createClass({
|
||||
// Velocity requires
|
||||
this._readMarkerGhostNode = null;
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
this._isMounted = true;
|
||||
},
|
||||
|
||||
@@ -154,15 +149,15 @@ module.exports = React.createClass({
|
||||
// 0: read marker is within the window
|
||||
// +1: read marker is below the window
|
||||
getReadMarkerPosition: function() {
|
||||
var readMarker = this.refs.readMarkerNode;
|
||||
var messageWrapper = this.refs.scrollPanel;
|
||||
const readMarker = this.refs.readMarkerNode;
|
||||
const messageWrapper = this.refs.scrollPanel;
|
||||
|
||||
if (!readMarker || !messageWrapper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
var readMarkerRect = readMarker.getBoundingClientRect();
|
||||
const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
const readMarkerRect = readMarker.getBoundingClientRect();
|
||||
|
||||
// the read-marker pretends to have zero height when it is actually
|
||||
// two pixels high; +2 here to account for that.
|
||||
@@ -241,6 +236,10 @@ module.exports = React.createClass({
|
||||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
_shouldShowEvent: function(mxEv) {
|
||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
@@ -249,7 +248,7 @@ module.exports = React.createClass({
|
||||
// Always show highlighted event
|
||||
if (this.props.highlightedEventId === mxEv.getId()) return true;
|
||||
|
||||
return !shouldHideEvent(mxEv, this._syncedSettings);
|
||||
return !shouldHideEvent(mxEv);
|
||||
},
|
||||
|
||||
_getEventTiles: function() {
|
||||
@@ -258,7 +257,7 @@ module.exports = React.createClass({
|
||||
|
||||
this.eventNodes = {};
|
||||
|
||||
var i;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
// actually going to show; this allows us to behave slightly
|
||||
@@ -268,9 +267,9 @@ module.exports = React.createClass({
|
||||
// a local echo, to manage the read-marker.
|
||||
let lastShownEvent;
|
||||
|
||||
var lastShownNonLocalEchoIndex = -1;
|
||||
let lastShownNonLocalEchoIndex = -1;
|
||||
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||
var mxEv = this.props.events[i];
|
||||
const mxEv = this.props.events[i];
|
||||
if (!this._shouldShowEvent(mxEv)) {
|
||||
continue;
|
||||
}
|
||||
@@ -288,12 +287,12 @@ module.exports = React.createClass({
|
||||
break;
|
||||
}
|
||||
|
||||
var ret = [];
|
||||
const ret = [];
|
||||
|
||||
var prevEvent = null; // the last event we showed
|
||||
let prevEvent = null; // the last event we showed
|
||||
|
||||
// assume there is no read marker until proven otherwise
|
||||
var readMarkerVisible = false;
|
||||
let readMarkerVisible = false;
|
||||
|
||||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
@@ -305,16 +304,16 @@ module.exports = React.createClass({
|
||||
const isMembershipChange = (e) => e.getType() === 'm.room.member';
|
||||
|
||||
for (i = 0; i < this.props.events.length; i++) {
|
||||
let mxEv = this.props.events[i];
|
||||
let eventId = mxEv.getId();
|
||||
let last = (mxEv === lastShownEvent);
|
||||
const mxEv = this.props.events[i];
|
||||
const eventId = mxEv.getId();
|
||||
const last = (mxEv === lastShownEvent);
|
||||
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
|
||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||
if (isMembershipChange(mxEv) && wantTile) {
|
||||
let readMarkerInMels = false;
|
||||
let ts1 = mxEv.getTs();
|
||||
const ts1 = mxEv.getTs();
|
||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||
// member events. This will prevent it from being re-created unnecessarily, and
|
||||
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||
@@ -326,7 +325,7 @@ module.exports = React.createClass({
|
||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
||||
let dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
|
||||
@@ -335,7 +334,7 @@ module.exports = React.createClass({
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
|
||||
let summarisedEvents = [mxEv];
|
||||
const summarisedEvents = [mxEv];
|
||||
for (;i + 1 < this.props.events.length; i++) {
|
||||
const collapsedMxEv = this.props.events[i + 1];
|
||||
|
||||
@@ -349,7 +348,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
if (!isMembershipChange(collapsedMxEv) ||
|
||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||
this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -361,35 +360,37 @@ module.exports = React.createClass({
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
||||
let highlightInMels = false;
|
||||
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
let eventTiles = summarisedEvents.map((e) => {
|
||||
if (e.getId() === this.props.highlightedEventId) {
|
||||
highlightInMels = true;
|
||||
}
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeperator is inserted.
|
||||
const ret = this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
prevEvent = e;
|
||||
return ret;
|
||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
}).reduce((a, b) => a.concat(b));
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
ret.push(<MemberEventListSummary key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
>
|
||||
{ eventTiles }
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -401,7 +402,7 @@ module.exports = React.createClass({
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
var isVisibleReadMarker = false;
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId == this.props.readMarkerEventId) {
|
||||
var visible = this.props.readMarkerVisible;
|
||||
@@ -441,10 +442,10 @@ module.exports = React.createClass({
|
||||
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
var ret = [];
|
||||
const ret = [];
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
let continuation = false;
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
@@ -469,8 +470,8 @@ module.exports = React.createClass({
|
||||
|
||||
// local echoes have a fake date, which could even be yesterday. Treat them
|
||||
// as 'today' for the date separators.
|
||||
var ts1 = mxEv.getTs();
|
||||
var eventDate = mxEv.getDate();
|
||||
let ts1 = mxEv.getTs();
|
||||
let eventDate = mxEv.getDate();
|
||||
if (mxEv.status) {
|
||||
eventDate = new Date();
|
||||
ts1 = eventDate.getTime();
|
||||
@@ -478,20 +479,20 @@ module.exports = React.createClass({
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour}/></li>;
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
continuation = false;
|
||||
}
|
||||
|
||||
var eventId = mxEv.getId();
|
||||
var highlight = (eventId == this.props.highlightedEventId);
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId == this.props.highlightedEventId);
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
var scrollToken = mxEv.status ? undefined : eventId;
|
||||
const scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
var readReceipts;
|
||||
if (this.props.manageReadReceipts) {
|
||||
let readReceipts;
|
||||
if (this.props.showReadReceipts) {
|
||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
}
|
||||
ret.push(
|
||||
@@ -508,8 +509,8 @@ module.exports = React.createClass({
|
||||
eventSendStatus={mxEv.status}
|
||||
tileShape={this.props.tileShape}
|
||||
isTwelveHour={this.props.isTwelveHour}
|
||||
last={last} isSelectedEvent={highlight}/>
|
||||
</li>
|
||||
last={last} isSelectedEvent={highlight} />
|
||||
</li>,
|
||||
);
|
||||
|
||||
return ret;
|
||||
@@ -544,12 +545,15 @@ module.exports = React.createClass({
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
let receipts = [];
|
||||
const receipts = [];
|
||||
room.getReceiptsForEvent(event).forEach((r) => {
|
||||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||
return; // ignore non-read receipts and receipts from self.
|
||||
}
|
||||
let member = room.getMember(r.userId);
|
||||
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
|
||||
return; // ignore ignored users
|
||||
}
|
||||
const member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
}
|
||||
@@ -565,7 +569,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_getReadMarkerTile: function(visible) {
|
||||
var hr;
|
||||
let hr;
|
||||
if (visible) {
|
||||
hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
@@ -575,7 +579,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<li key="_readupto" ref="readMarkerNode"
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{hr}
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
@@ -594,7 +598,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_getReadMarkerGhostTile: function() {
|
||||
var hr = <hr className="mx_RoomView_myReadMarker"
|
||||
const hr = <hr className="mx_RoomView_myReadMarker"
|
||||
style={{opacity: 1, width: '99%'}}
|
||||
ref={this._startAnimation}
|
||||
/>;
|
||||
@@ -605,7 +609,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||
className="mx_RoomView_myReadMarker_container">
|
||||
{hr}
|
||||
{ hr }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
@@ -617,7 +621,7 @@ module.exports = React.createClass({
|
||||
// once dynamic content in the events load, make the scrollPanel check the
|
||||
// scroll offsets.
|
||||
_onWidgetLoad: function() {
|
||||
var scrollPanel = this.refs.scrollPanel;
|
||||
const scrollPanel = this.refs.scrollPanel;
|
||||
if (scrollPanel) {
|
||||
scrollPanel.forceUpdate();
|
||||
}
|
||||
@@ -628,9 +632,9 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
var topSpinner, bottomSpinner;
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let topSpinner, bottomSpinner;
|
||||
if (this.props.backPaginating) {
|
||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||
}
|
||||
@@ -638,25 +642,26 @@ module.exports = React.createClass({
|
||||
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
||||
var style = this.props.hidden ? { display: 'none' } : {};
|
||||
style.opacity = this.props.opacity;
|
||||
const style = this.props.hidden ? { display: 'none' } : {};
|
||||
|
||||
var className = this.props.className + " mx_fadable";
|
||||
if (this.props.alwaysShowTimestamps) {
|
||||
className += " mx_MessagePanel_alwaysShowTimestamps";
|
||||
}
|
||||
const className = classNames(
|
||||
this.props.className,
|
||||
{
|
||||
"mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollPanel ref="scrollPanel" className={ className }
|
||||
onScroll={ this.props.onScroll }
|
||||
onResize={ this.onResize }
|
||||
onFillRequest={ this.props.onFillRequest }
|
||||
onUnfillRequest={ this.props.onUnfillRequest }
|
||||
style={ style }
|
||||
stickyBottom={ this.props.stickyBottom }>
|
||||
{topSpinner}
|
||||
{this._getEventTiles()}
|
||||
{bottomSpinner}
|
||||
<ScrollPanel ref="scrollPanel" className={className}
|
||||
onScroll={this.props.onScroll}
|
||||
onResize={this.onResize}
|
||||
onFillRequest={this.props.onFillRequest}
|
||||
onUnfillRequest={this.props.onUnfillRequest}
|
||||
style={style}
|
||||
stickyBottom={this.props.stickyBottom}>
|
||||
{ topSpinner }
|
||||
{ this._getEventTiles() }
|
||||
{ bottomSpinner }
|
||||
</ScrollPanel>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -15,33 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import sdk from '../../index';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import { _t } from '../../languageHandler';
|
||||
import dis from '../../dispatcher';
|
||||
import withMatrixClient from '../../wrappers/withMatrixClient';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import dis from '../../dispatcher';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../Modal';
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
||||
},
|
||||
});
|
||||
|
||||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'MyGroups',
|
||||
@@ -62,14 +41,18 @@ export default withMatrixClient(React.createClass({
|
||||
},
|
||||
|
||||
_onCreateGroupClick: function() {
|
||||
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
|
||||
Modal.createTrackedDialog('Create Group', '', CreateGroupDialog);
|
||||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
_fetch: function() {
|
||||
this.props.matrixClient.getJoinedGroups().done((result) => {
|
||||
this.setState({groups: result.groups, error: null});
|
||||
}, (err) => {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
// Indicate that the guest isn't in any groups (which should be true)
|
||||
this.setState({groups: [], error: null});
|
||||
return;
|
||||
}
|
||||
this.setState({groups: null, error: err});
|
||||
});
|
||||
},
|
||||
@@ -78,63 +61,71 @@ export default withMatrixClient(React.createClass({
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const GroupTile = sdk.getComponent("groups.GroupTile");
|
||||
|
||||
let content;
|
||||
let contentHeader;
|
||||
if (this.state.groups) {
|
||||
const groupNodes = [];
|
||||
this.state.groups.forEach((g) => {
|
||||
groupNodes.push(
|
||||
<div key={g}>
|
||||
<GroupTile groupId={g} />
|
||||
</div>,
|
||||
);
|
||||
groupNodes.push(<GroupTile groupId={g} />);
|
||||
});
|
||||
content = <div>
|
||||
<div>{_t('You are a member of these groups:')}</div>
|
||||
{groupNodes}
|
||||
</div>;
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</GeminiScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
"You're not currently a member of any communities.",
|
||||
) }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
content = <div className="mx_MyGroups_error">
|
||||
{_t('Error whilst fetching joined groups')}
|
||||
{ _t('Error whilst fetching joined communities') }
|
||||
</div>;
|
||||
} else {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return <div className="mx_MyGroups">
|
||||
<SimpleRoomHeader title={ _t("Groups") } />
|
||||
<div className='mx_MyGroups_joinCreateBox'>
|
||||
<div className="mx_MyGroups_createBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Create a new group')}
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||
<SimpleRoomHeader title={_t("Communities")} icon="img/icons-groups.svg" />
|
||||
<div className='mx_MyGroups_header'>
|
||||
<div className="mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_t(
|
||||
'Create a group to represent your community! '+
|
||||
'Define a set of rooms and your own custom homepage '+
|
||||
'to mark out your space in the Matrix universe.',
|
||||
)}
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Create a new community') }
|
||||
</div>
|
||||
{ _t(
|
||||
'Create a community to group together users and rooms! ' +
|
||||
'Build a custom homepage to mark out your space in the Matrix universe.',
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Join an existing group')}
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||
<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_tJsx(
|
||||
'To join an exisitng group you\'ll have to '+
|
||||
'know its group identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{sub}</i>,
|
||||
)}
|
||||
<div className="mx_MyGroups_headerCard_content">
|
||||
<div className="mx_MyGroups_headerCard_header">
|
||||
{ _t('Join an existing community') }
|
||||
</div>
|
||||
{ _t(
|
||||
'To join an existing community you\'ll have to '+
|
||||
'know its community identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
{},
|
||||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{content}
|
||||
{ contentHeader }
|
||||
{ content }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
@@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import { _t } from '../../languageHandler';
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var dis = require("../../dispatcher");
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
|
||||
/*
|
||||
* Component which shows the global notification list using a TimelinePanel
|
||||
*/
|
||||
var NotificationPanel = React.createClass({
|
||||
const NotificationPanel = React.createClass({
|
||||
displayName: 'NotificationPanel',
|
||||
|
||||
propTypes: {
|
||||
@@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
|
||||
|
||||
render: function() {
|
||||
// wrap a TimelinePanel with the jump-to-event bits turned off.
|
||||
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
|
||||
if (timelineSet) {
|
||||
return (
|
||||
<TimelinePanel key={"NotificationPanel_" + this.props.roomId}
|
||||
@@ -44,18 +44,16 @@ var NotificationPanel = React.createClass({
|
||||
manageReadReceipts={false}
|
||||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview = { false }
|
||||
opacity={ this.props.opacity }
|
||||
showUrlPreview = {false}
|
||||
tileShape="notif"
|
||||
empty={ _t('You have no visible notifications') }
|
||||
empty={_t('You have no visible notifications')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
console.error("No notifTimelineSet available!");
|
||||
return (
|
||||
<div className="mx_NotificationPanel">
|
||||
<Loader/>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,17 +16,26 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
||||
|
||||
const HIDE_DEBOUNCE_MS = 10000;
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
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({
|
||||
displayName: 'RoomStatusBar',
|
||||
|
||||
@@ -36,13 +46,14 @@ module.exports = React.createClass({
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
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
|
||||
// the end of the live timeline.
|
||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||
|
||||
// This is true when the user is alone in the room, but has also sent a message.
|
||||
// Used to suggest to the user to invite someone
|
||||
sentMessageAndIsAlone: React.PropTypes.bool,
|
||||
|
||||
// true if there is an active call in this room (means we show
|
||||
// the 'Active Call' text in the status bar if there is nothing
|
||||
// more interesting)
|
||||
@@ -60,6 +71,14 @@ module.exports = React.createClass({
|
||||
// 'unsent messages' bar
|
||||
onCancelAllClick: React.PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'invite others' button in the
|
||||
// 'you are alone' bar
|
||||
onInviteClick: React.PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'stop warning me' button in the
|
||||
// 'you are alone' bar
|
||||
onStopWarningClick: React.PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'scroll to bottom' button
|
||||
onScrollToBottomClick: React.PropTypes.func,
|
||||
|
||||
@@ -87,12 +106,14 @@ module.exports = React.createClass({
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
|
||||
this._checkSize();
|
||||
},
|
||||
@@ -103,10 +124,11 @@ module.exports = React.createClass({
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("sync", this.onSyncStateChange);
|
||||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
client.removeListener("Room.localEchoUpdated", this._onRoomLocalEchoUpdated);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,18 +137,38 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
syncState: state
|
||||
syncState: state,
|
||||
});
|
||||
},
|
||||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
_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
|
||||
_checkSize: function () {
|
||||
_checkSize: function() {
|
||||
if (this.props.onVisible && this._getSize()) {
|
||||
this.props.onVisible();
|
||||
}
|
||||
@@ -140,10 +182,11 @@ module.exports = React.createClass({
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
this.props.hasActiveCall
|
||||
this.props.hasActiveCall ||
|
||||
this.props.sentMessageAndIsAlone
|
||||
) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.props.unsentMessageError) {
|
||||
} else if (this.state.unsentMessages.length > 0) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
}
|
||||
return STATUS_BAR_HIDDEN;
|
||||
@@ -157,9 +200,9 @@ module.exports = React.createClass({
|
||||
if (this.props.numUnreadMessages) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/newmessages.svg" width="24" height="24"
|
||||
alt=""/>
|
||||
alt="" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -167,18 +210,18 @@ module.exports = React.createClass({
|
||||
if (!this.props.atEndOfLiveTimeline) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
<img src="img/scrolldown.svg" width="24" height="24"
|
||||
alt={ _t("Scroll to bottom of page") }
|
||||
title={ _t("Scroll to bottom of page") }/>
|
||||
alt={_t("Scroll to bottom of page")}
|
||||
title={_t("Scroll to bottom of page")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.hasActiveCall) {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
return (
|
||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20"/>
|
||||
<TintableSvg src="img/sound-indicator.svg" width="23" height="20" />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,7 +232,7 @@ module.exports = React.createClass({
|
||||
if (wantPlaceholder) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingIndicatorAvatars">
|
||||
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)}
|
||||
{ this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -221,14 +264,69 @@ module.exports = React.createClass({
|
||||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
|
||||
+{othersCount}
|
||||
</span>
|
||||
+{ othersCount }
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
@@ -240,59 +338,43 @@ module.exports = React.createClass({
|
||||
if (this.state.syncState === "ERROR") {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{_t('Connectivity to the server has been lost.')}
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{_t('Sent messages will be stored until your connection has returned.')}
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.unsentMessageError) {
|
||||
return (
|
||||
<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">
|
||||
{_tJsx("<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
|
||||
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
|
||||
[
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={ this.props.onResendAllClick }>{sub}</a>,
|
||||
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>,
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (this.state.unsentMessages.length > 0) {
|
||||
return this._getUnsentMessageContent();
|
||||
}
|
||||
|
||||
// unread count trumps who is typing since the unread count is only
|
||||
// set when you've scrolled up
|
||||
if (this.props.numUnreadMessages) {
|
||||
// MUST use var name "count" for pluralization to kick in
|
||||
var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||
const unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||
onClick={ this.props.onScrollToBottomClick }>
|
||||
{unreadMsgs}
|
||||
onClick={this.props.onScrollToBottomClick}>
|
||||
{ unreadMsgs }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
this.state.usersTyping,
|
||||
this.props.whoIsTypingLimit
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
if (typingString) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_typingBar">
|
||||
<EmojiText>{typingString}</EmojiText>
|
||||
<EmojiText>{ typingString }</EmojiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -300,7 +382,25 @@ module.exports = React.createClass({
|
||||
if (this.props.hasActiveCall) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_callBar">
|
||||
<b>{_t('Active call')}</b>
|
||||
<b>{ _t('Active call') }</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If you're alone in the room, and have sent a message, suggest to invite someone
|
||||
if (this.props.sentMessageAndIsAlone) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_isAlone">
|
||||
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
|
||||
"or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||
{},
|
||||
{
|
||||
'inviteText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
|
||||
'nowarnText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -308,17 +408,16 @@ module.exports = React.createClass({
|
||||
return null;
|
||||
},
|
||||
|
||||
|
||||
render: function() {
|
||||
var content = this._getContent();
|
||||
var indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
const content = this._getContent();
|
||||
const indicator = this._getIndicator(this.state.usersTyping.length > 0);
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
<div className="mx_RoomStatusBar_indicator">
|
||||
{indicator}
|
||||
{ indicator }
|
||||
</div>
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
import Promise from 'bluebird';
|
||||
var KeyCode = require('../../KeyCode');
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
const DEBUG_SCROLL = false;
|
||||
// var DEBUG_SCROLL = true;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
@@ -148,6 +148,7 @@ module.exports = React.createClass({
|
||||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onScroll: function() {},
|
||||
onResize: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -157,7 +158,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.checkFillState();
|
||||
this.checkScroll();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
@@ -178,7 +179,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onScroll: function(ev) {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
debuglog("Scroll event: offset now:", sn.scrollTop,
|
||||
"_lastSetScroll:", this._lastSetScroll);
|
||||
|
||||
@@ -238,7 +239,7 @@ module.exports = React.createClass({
|
||||
// about whether the the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom: function() {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
|
||||
// there seems to be some bug with flexbox/gemini/chrome/richvdh's
|
||||
// understanding of the box model, wherein the scrollNode ends up 2
|
||||
@@ -281,7 +282,7 @@ module.exports = React.createClass({
|
||||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight: function(backwards) {
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
if (backwards) {
|
||||
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
|
||||
} else {
|
||||
@@ -295,7 +296,7 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
var sn = this._getScrollNode();
|
||||
const sn = this._getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
// viewport, try to get some more messages.
|
||||
@@ -377,7 +378,7 @@ module.exports = React.createClass({
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill: function(backwards) {
|
||||
var dir = backwards ? 'b' : 'f';
|
||||
const dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||
return;
|
||||
@@ -470,8 +471,8 @@ module.exports = React.createClass({
|
||||
* mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
var delta = mult * scrollNode.clientHeight * 0.5;
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
this._setScrollTop(scrollNode.scrollTop + delta);
|
||||
this._saveScrollState();
|
||||
},
|
||||
@@ -535,7 +536,7 @@ module.exports = React.createClass({
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: scrollToken,
|
||||
pixelOffset: pixelOffset
|
||||
pixelOffset: pixelOffset,
|
||||
};
|
||||
|
||||
// ... then make it so.
|
||||
@@ -546,10 +547,10 @@ module.exports = React.createClass({
|
||||
// given offset in the window. A helper for _restoreSavedScrollState.
|
||||
_scrollToToken: function(scrollToken, pixelOffset) {
|
||||
/* find the dom node with the right scrolltoken */
|
||||
var node;
|
||||
var messages = this.refs.itemlist.children;
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var m = messages[i];
|
||||
let node;
|
||||
const messages = this.refs.itemlist.children;
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const m = messages[i];
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
// There might only be one scroll token
|
||||
if (m.dataset.scrollTokens &&
|
||||
@@ -564,18 +565,17 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollNode = this._getScrollNode();
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
const scrollNode = this._getScrollNode();
|
||||
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
|
||||
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
|
||||
pixelOffset + " (delta: "+scrollDelta+")");
|
||||
|
||||
if(scrollDelta != 0) {
|
||||
if (scrollDelta != 0) {
|
||||
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
_saveScrollState: function() {
|
||||
@@ -585,16 +585,16 @@ module.exports = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
var itemlist = this.refs.itemlist;
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var messages = itemlist.children;
|
||||
const itemlist = this.refs.itemlist;
|
||||
const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
const messages = itemlist.children;
|
||||
let newScrollState = null;
|
||||
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var node = messages[i];
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const node = messages[i];
|
||||
if (!node.dataset.scrollTokens) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
newScrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
|
||||
@@ -619,8 +619,8 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
var scrollState = this.scrollState;
|
||||
var scrollNode = this._getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const scrollNode = this._getScrollNode();
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
this._setScrollTop(Number.MAX_VALUE);
|
||||
@@ -631,9 +631,9 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_setScrollTop: function(scrollTop) {
|
||||
var scrollNode = this._getScrollNode();
|
||||
const scrollNode = this._getScrollNode();
|
||||
|
||||
var prevScroll = scrollNode.scrollTop;
|
||||
const prevScroll = scrollNode.scrollTop;
|
||||
|
||||
// FF ignores attempts to set scrollTop to very large numbers
|
||||
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
|
||||
@@ -676,7 +676,7 @@ module.exports = React.createClass({
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{this.props.children}
|
||||
{ this.props.children }
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
|
||||
116
src/components/structures/TagPanel.js
Normal file
116
src/components/structures/TagPanel.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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 { MatrixClient } from 'matrix-js-sdk';
|
||||
import FilterStore from '../../stores/FilterStore';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
orderedTags: [],
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
|
||||
this._filterStoreToken = FilterStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedTags: FilterStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_onGroupMyMembership() {
|
||||
if (this.unmounted) return;
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
|
||||
},
|
||||
|
||||
onClick() {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
onCreateGroupClick(ev) {
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
onTagTileEndDrag() {
|
||||
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
|
||||
},
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
key={tag + '_' + index}
|
||||
tag={tag}
|
||||
selected={this.state.selectedTags.includes(tag)}
|
||||
onEndDrag={this.onTagTileEndDrag}
|
||||
/>;
|
||||
});
|
||||
return <div className="mx_TagPanel" onClick={this.onClick}>
|
||||
<div className="mx_TagPanel_tagTileContainer">
|
||||
{ tags }
|
||||
</div>
|
||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
export default TagPanel;
|
||||
@@ -15,27 +15,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require("react-dom");
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import Promise from 'bluebird';
|
||||
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var EventTimeline = Matrix.EventTimeline;
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
const EventTimeline = Matrix.EventTimeline;
|
||||
|
||||
var sdk = require('../../index');
|
||||
const sdk = require('../../index');
|
||||
import { _t } from '../../languageHandler';
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var dis = require("../../dispatcher");
|
||||
var ObjectUtils = require('../../ObjectUtils');
|
||||
var Modal = require("../../Modal");
|
||||
var UserActivity = require("../../UserActivity");
|
||||
var KeyCode = require('../../KeyCode');
|
||||
import UserSettingsStore from '../../UserSettingsStore';
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const dis = require("../../dispatcher");
|
||||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const Modal = require("../../Modal");
|
||||
const UserActivity = require("../../UserActivity");
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
|
||||
var PAGINATE_SIZE = 20;
|
||||
var INITIAL_SIZE = 20;
|
||||
const PAGINATE_SIZE = 20;
|
||||
const INITIAL_SIZE = 20;
|
||||
|
||||
var DEBUG = false;
|
||||
const DEBUG = false;
|
||||
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
@@ -59,6 +60,7 @@ var TimelinePanel = React.createClass({
|
||||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
manageReadMarkers: React.PropTypes.bool,
|
||||
@@ -88,9 +90,6 @@ var TimelinePanel = React.createClass({
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// maximum number of events to show in a timeline
|
||||
timelineCap: React.PropTypes.number,
|
||||
|
||||
@@ -131,8 +130,6 @@ var TimelinePanel = React.createClass({
|
||||
}
|
||||
}
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
return {
|
||||
events: [],
|
||||
timelineLoading: true, // track whether our room timeline is loading
|
||||
@@ -177,10 +174,10 @@ var TimelinePanel = React.createClass({
|
||||
clientSyncState: MatrixClientPeg.get().getSyncState(),
|
||||
|
||||
// should the event tiles have twelve hour times
|
||||
isTwelveHour: syncedSettings.showTwelveHourTimestamps,
|
||||
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||
|
||||
// always show timestamps on event tiles?
|
||||
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps,
|
||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -259,7 +256,7 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
if (client) {
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
|
||||
@@ -274,20 +271,20 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
onMessageListUnfillRequest: function(backwards, scrollToken) {
|
||||
// If backwards, unpaginate from the back (i.e. the start of the timeline)
|
||||
let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
debuglog("TimelinePanel: unpaginating events in direction", dir);
|
||||
|
||||
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
|
||||
// this particular event should be the first or last to be unpaginated.
|
||||
let eventId = scrollToken;
|
||||
const eventId = scrollToken;
|
||||
|
||||
let marker = this.state.events.findIndex(
|
||||
const marker = this.state.events.findIndex(
|
||||
(ev) => {
|
||||
return ev.getId() === eventId;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||
const count = backwards ? marker + 1 : this.state.events.length - marker;
|
||||
|
||||
if (count > 0) {
|
||||
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
|
||||
@@ -304,16 +301,16 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
// set off a pagination request.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
|
||||
if (!this.state[canPaginateKey]) {
|
||||
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if(!this._timelineWindow.canPaginate(dir)) {
|
||||
if (!this._timelineWindow.canPaginate(dir)) {
|
||||
debuglog("TimelinePanel: can't", dir, "paginate any further");
|
||||
this.setState({[canPaginateKey]: false});
|
||||
return Promise.resolve(false);
|
||||
@@ -327,7 +324,7 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||
|
||||
var newState = {
|
||||
const newState = {
|
||||
[paginatingKey]: false,
|
||||
[canPaginateKey]: r,
|
||||
events: this._getEvents(),
|
||||
@@ -335,17 +332,24 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
// moving the window in this direction may mean that we can now
|
||||
// paginate in the other where we previously could not.
|
||||
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||
if (!this.state[canPaginateOtherWayKey] &&
|
||||
this._timelineWindow.canPaginate(otherDirection)) {
|
||||
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
||||
newState[canPaginateOtherWayKey] = true;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
return r;
|
||||
// Don't resolve until the setState has completed: we need to let
|
||||
// the component update before we consider the pagination completed,
|
||||
// otherwise we'll end up paginating in all the history the js-sdk
|
||||
// has in memory because we never gave the component a chance to scroll
|
||||
// itself into the right place
|
||||
return new Promise((resolve) => {
|
||||
this.setState(newState, () => {
|
||||
resolve(r);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -376,6 +380,9 @@ var TimelinePanel = React.createClass({
|
||||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
break;
|
||||
case 'ignore_state_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -409,15 +416,15 @@ var TimelinePanel = React.createClass({
|
||||
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
|
||||
if (this.unmounted) { return; }
|
||||
|
||||
var events = this._timelineWindow.getEvents();
|
||||
var lastEv = events[events.length-1];
|
||||
const events = this._timelineWindow.getEvents();
|
||||
const lastEv = events[events.length-1];
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
events.push(...this.props.timelineSet.room.getPendingEvents());
|
||||
}
|
||||
|
||||
var updatedState = {events: events};
|
||||
const updatedState = {events: events};
|
||||
|
||||
if (this.props.manageReadMarkers) {
|
||||
// when a new event arrives when the user is not watching the
|
||||
@@ -428,12 +435,12 @@ var TimelinePanel = React.createClass({
|
||||
// read-marker when a remote echo of an event we have just sent takes
|
||||
// more than the timeout on userCurrentlyActive.
|
||||
//
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
var sender = ev.sender ? ev.sender.userId : null;
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const sender = ev.sender ? ev.sender.userId : null;
|
||||
var callback = null;
|
||||
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
|
||||
updatedState.readMarkerVisible = true;
|
||||
} else if(lastEv && this.getReadMarkerPosition() === 0) {
|
||||
} else if (lastEv && this.getReadMarkerPosition() === 0) {
|
||||
// we know we're stuckAtBottom, so we can advance the RM
|
||||
// immediately, to save a later render cycle
|
||||
|
||||
@@ -635,7 +642,7 @@ var TimelinePanel = React.createClass({
|
||||
// and we'll get confused when their ID changes and we can't figure out
|
||||
// where the RM is pointing to. The read marker will be invisible for
|
||||
// now anyway, so this doesn't really matter.
|
||||
var lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
const lastDisplayedIndex = this._getLastDisplayedEventIndex({
|
||||
allowPartial: true,
|
||||
ignoreEchoes: true,
|
||||
});
|
||||
@@ -644,13 +651,13 @@ var TimelinePanel = React.createClass({
|
||||
return;
|
||||
}
|
||||
|
||||
var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
|
||||
this._setReadMarker(lastDisplayedEvent.getId(),
|
||||
lastDisplayedEvent.getTs());
|
||||
|
||||
// the read-marker should become invisible, so that if the user scrolls
|
||||
// down, they don't see it.
|
||||
if(this.state.readMarkerVisible) {
|
||||
if (this.state.readMarkerVisible) {
|
||||
this.setState({
|
||||
readMarkerVisible: false,
|
||||
});
|
||||
@@ -665,7 +672,7 @@ var TimelinePanel = React.createClass({
|
||||
// we call _timelineWindow.getEvents() rather than using
|
||||
// this.state.events, because react batches the update to the latter, so it
|
||||
// may not have been updated yet.
|
||||
var events = this._timelineWindow.getEvents();
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// first find where the current RM is
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
@@ -678,7 +685,7 @@ var TimelinePanel = React.createClass({
|
||||
}
|
||||
|
||||
// now think about advancing it
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
for (i++; i < events.length; i++) {
|
||||
var ev = events[i];
|
||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
||||
@@ -723,7 +730,7 @@ var TimelinePanel = React.createClass({
|
||||
//
|
||||
// a quick way to figure out if we've loaded the relevant event is
|
||||
// simply to check if the messagepanel knows where the read-marker is.
|
||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
// The messagepanel knows where the RM is, so we must have loaded
|
||||
// the relevant event.
|
||||
@@ -744,13 +751,13 @@ var TimelinePanel = React.createClass({
|
||||
forgetReadMarker: function() {
|
||||
if (!this.props.manageReadMarkers) return;
|
||||
|
||||
var rmId = this._getCurrentReadReceipt();
|
||||
const rmId = this._getCurrentReadReceipt();
|
||||
|
||||
// see if we know the timestamp for the rr event
|
||||
var tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||
var rmTs;
|
||||
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
|
||||
let rmTs;
|
||||
if (tl) {
|
||||
var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||
const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
|
||||
if (event) {
|
||||
rmTs = event.getTs();
|
||||
}
|
||||
@@ -790,7 +797,7 @@ var TimelinePanel = React.createClass({
|
||||
if (!this.props.manageReadMarkers) return null;
|
||||
if (!this.refs.messagePanel) return null;
|
||||
|
||||
var ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
const ret = this.refs.messagePanel.getReadMarkerPosition();
|
||||
if (ret !== null) {
|
||||
return ret;
|
||||
}
|
||||
@@ -833,8 +840,7 @@ var TimelinePanel = React.createClass({
|
||||
// jump to the live timeline on ctrl-end, rather than the end of the
|
||||
// timeline window.
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
|
||||
ev.keyCode == KeyCode.END)
|
||||
{
|
||||
ev.keyCode == KeyCode.END) {
|
||||
this.jumpToLiveTimeline();
|
||||
} else {
|
||||
this.refs.messagePanel.handleScrollKey(ev);
|
||||
@@ -842,12 +848,12 @@ var TimelinePanel = React.createClass({
|
||||
},
|
||||
|
||||
_initTimeline: function(props) {
|
||||
var initialEvent = props.eventId;
|
||||
var pixelOffset = props.eventPixelOffset;
|
||||
const initialEvent = props.eventId;
|
||||
const pixelOffset = props.eventPixelOffset;
|
||||
|
||||
// if a pixelOffset is given, it is relative to the bottom of the
|
||||
// container. If not, put the event in the middle of the container.
|
||||
var offsetBase = 1;
|
||||
let offsetBase = 1;
|
||||
if (pixelOffset == null) {
|
||||
offsetBase = 0.5;
|
||||
}
|
||||
@@ -876,7 +882,7 @@ var TimelinePanel = React.createClass({
|
||||
MatrixClientPeg.get(), this.props.timelineSet,
|
||||
{windowLimit: this.props.timelineCap});
|
||||
|
||||
var onLoaded = () => {
|
||||
const onLoaded = () => {
|
||||
this._reloadEvents();
|
||||
|
||||
// If we switched away from the room while there were pending
|
||||
@@ -911,15 +917,15 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
};
|
||||
|
||||
var onError = (error) => {
|
||||
const onError = (error) => {
|
||||
this.setState({timelineLoading: false});
|
||||
console.error(
|
||||
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||
);
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const msg = error.message ? error.message : JSON.stringify(error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
var onFinished;
|
||||
let onFinished;
|
||||
|
||||
// if we were given an event ID, then when the user closes the
|
||||
// dialog, let's jump to the end of the timeline. If we weren't,
|
||||
@@ -934,7 +940,7 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
};
|
||||
}
|
||||
var message = (error.errcode == 'M_FORBIDDEN')
|
||||
const message = (error.errcode == 'M_FORBIDDEN')
|
||||
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
|
||||
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
|
||||
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||
@@ -944,7 +950,7 @@ var TimelinePanel = React.createClass({
|
||||
});
|
||||
};
|
||||
|
||||
var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
|
||||
|
||||
// if we already have the event in question, TimelineWindow.load
|
||||
// returns a resolved promise.
|
||||
@@ -985,7 +991,7 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
// get the list of events from the timeline window and the pending event list
|
||||
_getEvents: function() {
|
||||
var events = this._timelineWindow.getEvents();
|
||||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// if we're at the end of the live timeline, append the pending events
|
||||
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||
@@ -996,7 +1002,7 @@ var TimelinePanel = React.createClass({
|
||||
},
|
||||
|
||||
_indexForEventId: function(evId) {
|
||||
for (var i = 0; i < this.state.events.length; ++i) {
|
||||
for (let i = 0; i < this.state.events.length; ++i) {
|
||||
if (evId == this.state.events[i].getId()) {
|
||||
return i;
|
||||
}
|
||||
@@ -1006,18 +1012,18 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
_getLastDisplayedEventIndex: function(opts) {
|
||||
opts = opts || {};
|
||||
var ignoreOwn = opts.ignoreOwn || false;
|
||||
var ignoreEchoes = opts.ignoreEchoes || false;
|
||||
var allowPartial = opts.allowPartial || false;
|
||||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const ignoreEchoes = opts.ignoreEchoes || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
||||
var messagePanel = this.refs.messagePanel;
|
||||
const messagePanel = this.refs.messagePanel;
|
||||
if (messagePanel === undefined) return null;
|
||||
|
||||
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||
var ev = this.state.events[i];
|
||||
for (let i = this.state.events.length-1; i >= 0; --i) {
|
||||
const ev = this.state.events[i];
|
||||
|
||||
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
|
||||
continue;
|
||||
@@ -1028,10 +1034,10 @@ var TimelinePanel = React.createClass({
|
||||
continue;
|
||||
}
|
||||
|
||||
var node = messagePanel.getNodeForEventId(ev.getId());
|
||||
const node = messagePanel.getNodeForEventId(ev.getId());
|
||||
if (!node) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
|
||||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
|
||||
return i;
|
||||
@@ -1049,18 +1055,18 @@ var TimelinePanel = React.createClass({
|
||||
* SDK.
|
||||
*/
|
||||
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||
var client = MatrixClientPeg.get();
|
||||
const client = MatrixClientPeg.get();
|
||||
// the client can be null on logout
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var myUserId = client.credentials.userId;
|
||||
const myUserId = client.credentials.userId;
|
||||
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||
},
|
||||
|
||||
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
|
||||
var roomId = this.props.timelineSet.room.roomId;
|
||||
const roomId = this.props.timelineSet.room.roomId;
|
||||
|
||||
// don't update the state (and cause a re-render) if there is
|
||||
// no change to the RM.
|
||||
@@ -1085,8 +1091,8 @@ var TimelinePanel = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
||||
// just show a spinner while the timeline loads.
|
||||
//
|
||||
@@ -1101,7 +1107,7 @@ var TimelinePanel = React.createClass({
|
||||
// exist.
|
||||
if (this.state.timelineLoading) {
|
||||
return (
|
||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
@@ -1109,7 +1115,7 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
|
||||
return (
|
||||
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }>
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<div className="mx_RoomView_empty">{ this.props.empty }</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1123,7 +1129,7 @@ var TimelinePanel = React.createClass({
|
||||
// forwards, otherwise if somebody hits the bottom of the loaded
|
||||
// events when viewing historical messages, we get stuck in a loop
|
||||
// of paginating our way through the entire history of the room.
|
||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||
|
||||
// If the state is PREPARED, we're still waiting for the js-sdk to sync with
|
||||
// the HS and fetch the latest events, so we are effectively forward paginating.
|
||||
@@ -1132,26 +1138,25 @@ var TimelinePanel = React.createClass({
|
||||
);
|
||||
return (
|
||||
<MessagePanel ref="messagePanel"
|
||||
hidden={ this.props.hidden }
|
||||
backPaginating={ this.state.backPaginating }
|
||||
forwardPaginating={ forwardPaginating }
|
||||
events={ this.state.events }
|
||||
highlightedEventId={ this.props.highlightedEventId }
|
||||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
manageReadReceipts = { this.props.manageReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
onFillRequest={ this.onMessageListFillRequest }
|
||||
onUnfillRequest={ this.onMessageListUnfillRequest }
|
||||
opacity={ this.props.opacity }
|
||||
isTwelveHour={ this.state.isTwelveHour }
|
||||
alwaysShowTimestamps={ this.state.alwaysShowTimestamps }
|
||||
className={ this.props.className }
|
||||
tileShape={ this.props.tileShape }
|
||||
hidden={this.props.hidden}
|
||||
backPaginating={this.state.backPaginating}
|
||||
forwardPaginating={forwardPaginating}
|
||||
events={this.state.events}
|
||||
highlightedEventId={this.props.highlightedEventId}
|
||||
readMarkerEventId={this.state.readMarkerEventId}
|
||||
readMarkerVisible={this.state.readMarkerVisible}
|
||||
suppressFirstDateSeparator={this.state.canBackPaginate}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
showReadReceipts={this.props.showReadReceipts}
|
||||
ourUserId={MatrixClientPeg.get().credentials.userId}
|
||||
stickyBottom={stickyBottom}
|
||||
onScroll={this.onMessageListScroll}
|
||||
onFillRequest={this.onMessageListFillRequest}
|
||||
onUnfillRequest={this.onMessageListUnfillRequest}
|
||||
isTwelveHour={this.state.isTwelveHour}
|
||||
alwaysShowTimestamps={this.state.alwaysShowTimestamps}
|
||||
className={this.props.className}
|
||||
tileShape={this.props.tileShape}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ContentMessages = require('../../ContentMessages');
|
||||
var dis = require('../../dispatcher');
|
||||
var filesize = require('filesize');
|
||||
const React = require('react');
|
||||
const ContentMessages = require('../../ContentMessages');
|
||||
const dis = require('../../dispatcher');
|
||||
const filesize = require('filesize');
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: React.PropTypes.object
|
||||
room: React.PropTypes.object,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@@ -46,7 +46,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var uploads = ContentMessages.getCurrentUploads();
|
||||
const uploads = ContentMessages.getCurrentUploads();
|
||||
|
||||
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
|
||||
// check in RoomView
|
||||
@@ -62,8 +62,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||
return <div />;
|
||||
}
|
||||
|
||||
var upload;
|
||||
for (var i = 0; i < uploads.length; ++i) {
|
||||
let upload;
|
||||
for (let i = 0; i < uploads.length; ++i) {
|
||||
if (uploads[i].roomId == this.props.room.roomId) {
|
||||
upload = uploads[i];
|
||||
break;
|
||||
@@ -73,32 +73,32 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||
return <div />;
|
||||
}
|
||||
|
||||
var innerProgressStyle = {
|
||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
|
||||
const innerProgressStyle = {
|
||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
|
||||
};
|
||||
var uploadedSize = filesize(upload.loaded);
|
||||
var totalSize = filesize(upload.total);
|
||||
let uploadedSize = filesize(upload.loaded);
|
||||
const totalSize = filesize(upload.total);
|
||||
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
|
||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||
}
|
||||
|
||||
// MUST use var name 'count' for pluralization to kick in
|
||||
var uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||
const uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)});
|
||||
|
||||
return (
|
||||
<div className="mx_UploadBar">
|
||||
<div className="mx_UploadBar_uploadProgressOuter">
|
||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||
</div>
|
||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22"/>
|
||||
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src="img/fileicon.png" width="17" height="22" />
|
||||
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
|
||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||
/>
|
||||
<div className="mx_UploadBar_uploadBytes">
|
||||
{ uploadedSize } / { totalSize }
|
||||
</div>
|
||||
<div className="mx_UploadBar_uploadFilename">{uploadText}</div>
|
||||
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +15,8 @@ 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 SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const sdk = require('../../index');
|
||||
@@ -32,7 +35,7 @@ const AddThreepid = require('../../AddThreepid');
|
||||
const SdkConfig = require('../../SdkConfig');
|
||||
import Analytics from '../../Analytics';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import * as languageHandler from '../../languageHandler';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
@@ -52,126 +55,102 @@ const gHVersionLabel = function(repo, token='') {
|
||||
} else {
|
||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||
}
|
||||
return <a target="_blank" rel="noopener" href={url}>{token}</a>;
|
||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||
};
|
||||
|
||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
// 'label' is how we describe it in the UI.
|
||||
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
|
||||
// since they will be translated when rendered.
|
||||
const SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: 'Hide read receipts',
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: "Don't send typing notifications",
|
||||
},
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
label: 'Always show message timestamps',
|
||||
},
|
||||
{
|
||||
id: 'showTwelveHourTimestamps',
|
||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||
},
|
||||
{
|
||||
id: 'hideJoinLeaves',
|
||||
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
||||
},
|
||||
{
|
||||
id: 'hideAvatarDisplaynameChanges',
|
||||
label: 'Hide avatar and display name changes',
|
||||
},
|
||||
{
|
||||
id: 'useCompactLayout',
|
||||
label: 'Use compact timeline layout',
|
||||
},
|
||||
{
|
||||
id: 'hideRedactions',
|
||||
label: 'Hide removed messages',
|
||||
},
|
||||
{
|
||||
id: 'enableSyntaxHighlightLanguageDetection',
|
||||
label: 'Enable automatic language detection for syntax highlighting',
|
||||
},
|
||||
{
|
||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||
label: 'Automatically replace plain text Emoji',
|
||||
},
|
||||
{
|
||||
id: 'Pill.shouldHidePillAvatar',
|
||||
label: 'Hide avatars in user and room mentions',
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: 'useFixedWidthFont',
|
||||
label: 'Use fixed width font',
|
||||
},
|
||||
*/
|
||||
// Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
|
||||
// must be settings defined in SettingsStore.
|
||||
const SIMPLE_SETTINGS = [
|
||||
{ id: "urlPreviewsEnabled" },
|
||||
{ id: "autoplayGifsAndVideos" },
|
||||
{ id: "hideReadReceipts" },
|
||||
{ id: "dontSendTypingNotifications" },
|
||||
{ id: "alwaysShowTimestamps" },
|
||||
{ id: "showTwelveHourTimestamps" },
|
||||
{ id: "hideJoinLeaves" },
|
||||
{ id: "hideAvatarChanges" },
|
||||
{ id: "hideDisplaynameChanges" },
|
||||
{ id: "useCompactLayout" },
|
||||
{ id: "hideRedactions" },
|
||||
{ id: "enableSyntaxHighlightLanguageDetection" },
|
||||
{ id: "MessageComposerInput.autoReplaceEmoji" },
|
||||
{ id: "MessageComposerInput.dontSuggestEmoji" },
|
||||
{ id: "Pill.shouldHidePillAvatar" },
|
||||
{ id: "TextualBody.disableBigEmoji" },
|
||||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
];
|
||||
|
||||
const ANALYTICS_SETTINGS_LABELS = [
|
||||
// These settings must be defined in SettingsStore
|
||||
const ANALYTICS_SETTINGS = [
|
||||
{
|
||||
id: 'analyticsOptOut',
|
||||
label: 'Opt out of analytics',
|
||||
fn: function(checked) {
|
||||
Analytics[checked ? 'disable' : 'enable']();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const WEBRTC_SETTINGS_LABELS = [
|
||||
// These settings must be defined in SettingsStore
|
||||
const WEBRTC_SETTINGS = [
|
||||
{
|
||||
id: 'webRtcForceTURN',
|
||||
label: 'Disable Peer-to-Peer for 1:1 calls',
|
||||
fn: (val) => {
|
||||
MatrixClientPeg.get().setForceTURN(val);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
|
||||
// since they will be translated when rendered.
|
||||
const CRYPTO_SETTINGS_LABELS = [
|
||||
// These settings must be defined in SettingsStore
|
||||
const CRYPTO_SETTINGS = [
|
||||
{
|
||||
id: 'blacklistUnverifiedDevices',
|
||||
label: 'Never send encrypted messages to unverified devices from this device',
|
||||
fn: function(checked) {
|
||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||
},
|
||||
},
|
||||
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
|
||||
// {
|
||||
// id: 'blacklistUnverifiedDevicesPerRoom'
|
||||
// label: 'Never send encrypted messages to unverified devices in this room',
|
||||
// }
|
||||
];
|
||||
|
||||
// Enumerate the available themes, with a nice human text label.
|
||||
// 'id' gives the key name in the im.vector.web.settings account data event
|
||||
// 'value' is the value for that key in the event
|
||||
// 'label' is how we describe it in the UI.
|
||||
// 'value' is the value for the theme setting
|
||||
//
|
||||
// XXX: Ideally we would have a theme manifest or something and they'd be nicely
|
||||
// packaged up in a single directory, and/or located at the application layer.
|
||||
// But for now for expedience we just hardcode them here.
|
||||
const THEMES = [
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Light theme',
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Dark theme',
|
||||
value: 'dark',
|
||||
},
|
||||
{ label: _td('Light theme'), value: 'light' },
|
||||
{ label: _td('Dark theme'), value: 'dark' },
|
||||
{ label: _td('Status.im theme'), value: 'status' },
|
||||
];
|
||||
|
||||
const IgnoredUser = React.createClass({
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
onUnignored: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_onUnignoreClick: function() {
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(this.props.userId);
|
||||
if (index !== -1) {
|
||||
ignoredUsers.splice(index, 1);
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers)
|
||||
.then(() => this.props.onUnignored(this.props.userId));
|
||||
} else this.props.onUnignored(this.props.userId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<li>
|
||||
<AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
|
||||
{ _t("Unignore") }
|
||||
</AccessibleButton>
|
||||
{ this.props.userId }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
||||
@@ -180,9 +159,6 @@ module.exports = React.createClass({
|
||||
// The brand string given when creating email pushers
|
||||
brand: React.PropTypes.string,
|
||||
|
||||
// True to show the 'labs' section of experimental features
|
||||
enableLabs: React.PropTypes.bool,
|
||||
|
||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||
referralBaseUrl: React.PropTypes.string,
|
||||
|
||||
@@ -194,7 +170,6 @@ module.exports = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onClose: function() {},
|
||||
enableLabs: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -207,6 +182,7 @@ module.exports = React.createClass({
|
||||
vectorVersion: undefined,
|
||||
rejectingInvites: false,
|
||||
mediaDevices: null,
|
||||
ignoredUsers: [],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -228,6 +204,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
this._refreshMediaDevices();
|
||||
this._refreshIgnoredUsers();
|
||||
|
||||
// Bulk rejecting invites:
|
||||
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||
@@ -236,20 +213,12 @@ module.exports = React.createClass({
|
||||
MatrixClientPeg.get().on("RoomMember.membership", this._onInviteStateChange);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 0.3,
|
||||
middleOpacity: 0.3,
|
||||
action: 'panel_disable',
|
||||
sideDisabled: true,
|
||||
middleDisabled: true,
|
||||
});
|
||||
this._refreshFromServer();
|
||||
|
||||
const syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
if (!syncedSettings.theme) {
|
||||
syncedSettings.theme = 'light';
|
||||
}
|
||||
this._syncedSettings = syncedSettings;
|
||||
|
||||
this._localSettings = UserSettingsStore.getLocalSettings();
|
||||
|
||||
if (PlatformPeg.get().isElectron()) {
|
||||
const {ipcRenderer} = require('electron');
|
||||
|
||||
@@ -276,9 +245,9 @@ module.exports = React.createClass({
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
action: 'panel_disable',
|
||||
sideDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
dis.unregister(this.dispatcherRef);
|
||||
const cli = MatrixClientPeg.get();
|
||||
@@ -320,8 +289,8 @@ module.exports = React.createClass({
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
mediaDevices,
|
||||
activeAudioInput: this._localSettings['webrtc_audioinput'],
|
||||
activeVideoInput: this._localSettings['webrtc_videoinput'],
|
||||
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -346,9 +315,22 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_refreshIgnoredUsers: function(userIdUnignored=null) {
|
||||
const users = MatrixClientPeg.get().getIgnoredUsers();
|
||||
if (userIdUnignored) {
|
||||
const index = users.indexOf(userIdUnignored);
|
||||
if (index !== -1) users.splice(index, 1);
|
||||
}
|
||||
this.setState({
|
||||
ignoredUsers: users,
|
||||
});
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action === "notifier_enabled") {
|
||||
this.forceUpdate();
|
||||
} else if (payload.action === "ignore_state_changed") {
|
||||
this._refreshIgnoredUsers();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -379,6 +361,11 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
onAvatarRemoveClick: function() {
|
||||
MatrixClientPeg.get().setAvatarUrl(null);
|
||||
this.setState({avatarUrl: null}); // the avatar update will complete async for us
|
||||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
|
||||
@@ -388,7 +375,7 @@ module.exports = React.createClass({
|
||||
{ _t("For security, logging out will delete any end-to-end " +
|
||||
"encryption keys from this browser. If you want to be able " +
|
||||
"to decrypt your conversation history from future Riot sessions, " +
|
||||
"please export your room keys for safe-keeping.") }.
|
||||
"please export your room keys for safe-keeping.") }
|
||||
</div>,
|
||||
button: _t("Sign out"),
|
||||
extraButtons: [
|
||||
@@ -435,10 +422,6 @@ module.exports = React.createClass({
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
},
|
||||
|
||||
onEnableNotificationsChange: function(event) {
|
||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
},
|
||||
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
if (!shouldSubmit) return;
|
||||
this._addEmail();
|
||||
@@ -612,6 +595,11 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_renderGroupSettings: function() {
|
||||
const GroupUserSettings = sdk.getComponent('groups.GroupUserSettings');
|
||||
return <GroupUserSettings />;
|
||||
},
|
||||
|
||||
_renderReferral: function() {
|
||||
const teamToken = this.props.teamToken;
|
||||
if (!teamToken) {
|
||||
@@ -627,15 +615,15 @@ module.exports = React.createClass({
|
||||
<div>
|
||||
<h3>Referral</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
|
||||
{ _t("Refer a friend to Riot:") } <a href={href}>{ href }</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
onLanguageChange: function(newLang) {
|
||||
if(this.state.language !== newLang) {
|
||||
UserSettingsStore.setLocalSetting('language', newLang);
|
||||
if (this.state.language !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
this.setState({
|
||||
language: newLang,
|
||||
});
|
||||
@@ -646,7 +634,7 @@ module.exports = React.createClass({
|
||||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div>
|
||||
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
|
||||
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
|
||||
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
||||
className="mx_UserSettings_language"
|
||||
value={this.state.language}
|
||||
@@ -658,22 +646,21 @@ module.exports = React.createClass({
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) =>
|
||||
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
|
||||
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Interface") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ this._renderUrlPreviewSelector() }
|
||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||
{ THEMES.map( this._renderThemeSelector ) }
|
||||
{ SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
|
||||
{ THEMES.map( this._renderThemeOption ) }
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
||||
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
|
||||
defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</td>
|
||||
@@ -686,68 +673,31 @@ module.exports = React.createClass({
|
||||
);
|
||||
},
|
||||
|
||||
_renderUrlPreviewSelector: function() {
|
||||
return <div className="mx_UserSettings_toggle">
|
||||
<input id="urlPreviewsDisabled"
|
||||
type="checkbox"
|
||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||
onChange={ this._onPreviewsDisabledChanged }
|
||||
/>
|
||||
<label htmlFor="urlPreviewsDisabled">
|
||||
{ _t("Disable inline URL previews by default") }
|
||||
</label>
|
||||
</div>;
|
||||
_renderAccountSetting: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<SettingsFlag name={setting.id}
|
||||
label={setting.label}
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={setting.fn} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_onPreviewsDisabledChanged: function(e) {
|
||||
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked);
|
||||
},
|
||||
|
||||
_renderSyncedSetting: function(setting) {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
|
||||
const onChange = (e) => {
|
||||
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
|
||||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
type="checkbox"
|
||||
defaultChecked={ this._syncedSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderThemeSelector: function(setting) {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'set_theme',
|
||||
value: setting.value,
|
||||
});
|
||||
};
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
||||
<input id={ setting.id + "_" + setting.value }
|
||||
type="radio"
|
||||
name={ setting.id }
|
||||
value={ setting.value }
|
||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
||||
{ setting.label }
|
||||
</label>
|
||||
</div>;
|
||||
_renderThemeOption: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
|
||||
<SettingsFlag name="theme"
|
||||
label={setting.label}
|
||||
level={SettingLevel.ACCOUNT}
|
||||
onChange={onChange}
|
||||
group="theme"
|
||||
value={setting.value} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderCryptoInfo: function() {
|
||||
@@ -781,46 +731,58 @@ module.exports = React.createClass({
|
||||
<h3>{ _t("Cryptography") }</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device ID:")}</label>
|
||||
<span><code>{deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key:")}</label>
|
||||
<span><code><b>{identityKey}</b></code></span></li>
|
||||
<li><label>{ _t("Device ID:") }</label>
|
||||
<span><code>{ deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key:") }</label>
|
||||
<span><code><b>{ identityKey }</b></code></span></li>
|
||||
</ul>
|
||||
{ importExportButtons }
|
||||
</div>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||
{ CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderLocalSetting: function(setting) {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
|
||||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
_renderIgnoredUsers: function() {
|
||||
if (this.state.ignoredUsers.length > 0) {
|
||||
const updateHandler = this._refreshIgnoredUsers;
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Ignored Users") }</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_ignoredUsersSection">
|
||||
<ul>
|
||||
{ this.state.ignoredUsers.map(function(userId) {
|
||||
return (<IgnoredUser key={userId}
|
||||
userId={userId}
|
||||
onUnignored={updateHandler}></IgnoredUser>);
|
||||
}) }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else return (<div />);
|
||||
},
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
type="checkbox"
|
||||
defaultChecked={ this._localSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
_renderDeviceSetting: function(setting) {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
return (
|
||||
<div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<SettingsFlag name={setting.id}
|
||||
label={setting.label}
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={setting.fn} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderDevicesPanel: function() {
|
||||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
<h3>{_t("Devices")}</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section"/>
|
||||
<h3>{ _t("Devices") }</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -835,7 +797,7 @@ module.exports = React.createClass({
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("Found a bug?") }</p>
|
||||
<button className="mx_UserSettings_button danger"
|
||||
onClick={this._onBugReportClicked}>{_t('Report it')}
|
||||
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -843,46 +805,37 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_renderAnalyticsControl: function() {
|
||||
if (!SdkConfig.get().piwik) return <div/>;
|
||||
if (!SdkConfig.get().piwik) return <div />;
|
||||
|
||||
return <div>
|
||||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
|
||||
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
|
||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderLabs: function() {
|
||||
// default to enabled if undefined
|
||||
if (this.props.enableLabs === false) return null;
|
||||
UserSettingsStore.doTranslations();
|
||||
|
||||
const features = [];
|
||||
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
|
||||
// This feature has an override and will be set to the default, so do not
|
||||
// show it here.
|
||||
if (feature.override) {
|
||||
return;
|
||||
}
|
||||
SettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked);
|
||||
SettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
features.push(
|
||||
<div key={feature.id} className="mx_UserSettings_toggle">
|
||||
<div key={featureId} className="mx_UserSettings_toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
||||
onChange={ onChange }
|
||||
id={featureId}
|
||||
name={featureId}
|
||||
defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
|
||||
</div>);
|
||||
});
|
||||
|
||||
@@ -896,7 +849,7 @@ module.exports = React.createClass({
|
||||
<h3>{ _t("Labs") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
||||
{features}
|
||||
{ features }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -929,10 +882,10 @@ module.exports = React.createClass({
|
||||
const platform = PlatformPeg.get();
|
||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||
return <div>
|
||||
<h3>{_t('Updates')}</h3>
|
||||
<h3>{ _t('Updates') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||
{_t('Check for update')}
|
||||
{ _t('Check for update') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
@@ -958,7 +911,7 @@ module.exports = React.createClass({
|
||||
reject = (
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={onClick}>
|
||||
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
|
||||
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
@@ -966,7 +919,7 @@ module.exports = React.createClass({
|
||||
return <div>
|
||||
<h3>{ _t("Bulk Options") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{reject}
|
||||
{ reject }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
@@ -975,6 +928,8 @@ module.exports = React.createClass({
|
||||
const settings = this.state.electron_settings;
|
||||
if (!settings) return;
|
||||
|
||||
// TODO: This should probably be a granular setting, but it only applies to electron
|
||||
// and ends up being get/set outside of matrix anyways (local system setting).
|
||||
return <div>
|
||||
<h3>{ _t('Desktop specific') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
@@ -984,7 +939,7 @@ module.exports = React.createClass({
|
||||
defaultChecked={settings['auto-launch']}
|
||||
onChange={this._onAutoLaunchChanged}
|
||||
/>
|
||||
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
|
||||
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
@@ -996,7 +951,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_mapWebRtcDevicesToSpans: function(devices) {
|
||||
return devices.map((device) => <span key={device.deviceId}>{device.label}</span>);
|
||||
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||
},
|
||||
|
||||
_setAudioInput: function(deviceId) {
|
||||
@@ -1032,15 +987,15 @@ module.exports = React.createClass({
|
||||
if (this.state.mediaDevices === false) {
|
||||
return (
|
||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
{_t('Missing Media Permissions, click here to request.')}
|
||||
{ _t('Missing Media Permissions, click here to request.') }
|
||||
</p>
|
||||
);
|
||||
} else if (!this.state.mediaDevices) return;
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
|
||||
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
|
||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
const defaultOption = {
|
||||
deviceId: '',
|
||||
@@ -1057,12 +1012,12 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
microphoneDropdown = <div>
|
||||
<h4>{_t('Microphone')}</h4>
|
||||
<h4>{ _t('Microphone') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeAudioInput || defaultInput}
|
||||
onOptionChange={this._setAudioInput}>
|
||||
{this._mapWebRtcDevicesToSpans(audioInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(audioInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
@@ -1077,27 +1032,27 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
webcamDropdown = <div>
|
||||
<h4>{_t('Camera')}</h4>
|
||||
<h4>{ _t('Camera') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeVideoInput || defaultInput}
|
||||
onOptionChange={this._setVideoInput}>
|
||||
{this._mapWebRtcDevicesToSpans(videoInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(videoInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{microphoneDropdown}
|
||||
{webcamDropdown}
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderWebRtcSettings: function() {
|
||||
return <div>
|
||||
<h3>{_t('VoIP')}</h3>
|
||||
<h3>{ _t('VoIP') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
||||
{ WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
|
||||
{ this._renderWebRtcDeviceSettings() }
|
||||
</div>
|
||||
</div>;
|
||||
@@ -1161,7 +1116,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input type="text" key={val.address} id={id}
|
||||
@@ -1169,7 +1124,7 @@ module.exports = React.createClass({
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") }
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
onClick={onRemoveClick} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1182,16 +1137,16 @@ module.exports = React.createClass({
|
||||
addEmailSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label>{_t('Email')}</label>
|
||||
<label>{ _t('Email') }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<EditableText
|
||||
ref="add_email_input"
|
||||
className="mx_UserSettings_editable"
|
||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||
placeholder={ _t("Add email address") }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this._onAddEmailEditFinished } />
|
||||
placeholder={_t("Add email address")}
|
||||
blurToCancel={false}
|
||||
onValueChanged={this._onAddEmailEditFinished} />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
@@ -1239,8 +1194,8 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader
|
||||
title={ _t("Settings") }
|
||||
onCancelClick={ this.props.onClose }
|
||||
title={_t("Settings")}
|
||||
onCancelClick={this.props.onClose}
|
||||
/>
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body"
|
||||
@@ -1258,21 +1213,28 @@ module.exports = React.createClass({
|
||||
<ChangeDisplayName />
|
||||
</div>
|
||||
</div>
|
||||
{threepidsSection}
|
||||
{ threepidsSection }
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
|
||||
<img src="img/cancel.svg"
|
||||
width="15" height="15"
|
||||
className="mx_filterFlipColor"
|
||||
alt={_t("Remove avatar")}
|
||||
title={_t("Remove avatar")} />
|
||||
</div>
|
||||
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||
</div>
|
||||
<div className="mx_UserSettings_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src="img/camera.svg" className="mx_filterFlipColor"
|
||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1289,36 +1251,39 @@ module.exports = React.createClass({
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{accountJsx}
|
||||
{ accountJsx }
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
{ this._renderGroupSettings() }
|
||||
|
||||
{notificationArea}
|
||||
{ this._renderReferral() }
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
{this._renderWebRtcSettings()}
|
||||
{this._renderDevicesPanel()}
|
||||
{this._renderCryptoInfo()}
|
||||
{this._renderBulkOptions()}
|
||||
{this._renderBugReport()}
|
||||
{ notificationArea }
|
||||
|
||||
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
|
||||
{ this._renderUserInterfaceSettings() }
|
||||
{ this._renderLabs() }
|
||||
{ this._renderWebRtcSettings() }
|
||||
{ this._renderDevicesPanel() }
|
||||
{ this._renderCryptoInfo() }
|
||||
{ this._renderIgnoredUsers() }
|
||||
{ this._renderBulkOptions() }
|
||||
{ this._renderBugReport() }
|
||||
|
||||
{this._renderAnalyticsControl()}
|
||||
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||
|
||||
{ this._renderAnalyticsControl() }
|
||||
|
||||
<h3>{ _t("Advanced") }</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Logged in as:") } {this._me}
|
||||
{ _t("Logged in as:") } { this._me }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{_t('Access Token:')}
|
||||
{ _t('Access Token:') }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1329,23 +1294,23 @@ module.exports = React.createClass({
|
||||
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
|
||||
{ _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '<local>')
|
||||
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||
: REACT_SDK_VERSION
|
||||
}<br/>
|
||||
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
|
||||
}<br />
|
||||
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
|
||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||
: 'unknown'
|
||||
}<br/>
|
||||
{ _t("olm version:") } {olmVersionString}<br/>
|
||||
}<br />
|
||||
{ _t("olm version:") } { olmVersionString }<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this._renderCheckUpdate()}
|
||||
{ this._renderCheckUpdate() }
|
||||
|
||||
{this._renderClearCache()}
|
||||
{ this._renderClearCache() }
|
||||
|
||||
{this._renderDeactivateAccount()}
|
||||
{ this._renderDeactivateAccount() }
|
||||
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,13 +17,13 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
var sdk = require('../../../index');
|
||||
var Modal = require("../../../Modal");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
|
||||
var PasswordReset = require("../../../PasswordReset");
|
||||
import PasswordReset from "../../../PasswordReset";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
@@ -34,30 +35,30 @@ module.exports = React.createClass({
|
||||
customIsUrl: React.PropTypes.string,
|
||||
onLoginClick: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func,
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
progress: null
|
||||
progress: null,
|
||||
};
|
||||
},
|
||||
|
||||
submitPasswordReset: function(hsUrl, identityUrl, email, password) {
|
||||
this.setState({
|
||||
progress: "sending_email"
|
||||
progress: "sending_email",
|
||||
});
|
||||
this.reset = new PasswordReset(hsUrl, identityUrl);
|
||||
this.reset.resetPassword(email, password).done(() => {
|
||||
this.setState({
|
||||
progress: "sent_email"
|
||||
progress: "sent_email",
|
||||
});
|
||||
}, (err) => {
|
||||
this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
|
||||
this.setState({
|
||||
progress: null
|
||||
progress: null,
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -80,15 +81,12 @@ module.exports = React.createClass({
|
||||
|
||||
if (!this.state.email) {
|
||||
this.showErrorDialog(_t('The email address linked to your account must be entered.'));
|
||||
}
|
||||
else if (!this.state.password || !this.state.password2) {
|
||||
} else if (!this.state.password || !this.state.password2) {
|
||||
this.showErrorDialog(_t('A new password must be entered.'));
|
||||
}
|
||||
else if (this.state.password !== this.state.password2) {
|
||||
} else if (this.state.password !== this.state.password2) {
|
||||
this.showErrorDialog(_t('New passwords must match each other.'));
|
||||
}
|
||||
else {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description:
|
||||
@@ -98,7 +96,7 @@ module.exports = React.createClass({
|
||||
'end-to-end encryption keys on all devices, ' +
|
||||
'making encrypted chat history unreadable, ' +
|
||||
'unless you first export your room keys and re-import ' +
|
||||
'them afterwards. In future this will be improved.'
|
||||
'them afterwards. In future this will be improved.',
|
||||
) }
|
||||
</div>,
|
||||
button: _t('Continue'),
|
||||
@@ -106,13 +104,13 @@ module.exports = React.createClass({
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>
|
||||
</button>,
|
||||
],
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.submitPasswordReset(
|
||||
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
|
||||
this.state.email, this.state.password
|
||||
this.state.email, this.state.password,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -132,24 +130,23 @@ module.exports = React.createClass({
|
||||
|
||||
onInputChanged: function(stateKey, ev) {
|
||||
this.setState({
|
||||
[stateKey]: ev.target.value
|
||||
[stateKey]: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl
|
||||
});
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
|
||||
title: title,
|
||||
description: body,
|
||||
@@ -157,37 +154,49 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
var ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
var Spinner = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
var resetPasswordJsx;
|
||||
let resetPasswordJsx;
|
||||
|
||||
if (this.state.progress === "sending_email") {
|
||||
resetPasswordJsx = <Spinner />;
|
||||
}
|
||||
else if (this.state.progress === "sent_email") {
|
||||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you've followed the link it contains, click below') }.
|
||||
<div className="mx_Login_prompt">
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={ _t('I have verified my email address') } />
|
||||
value={_t('I have verified my email address')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else if (this.state.progress === "complete") {
|
||||
} else if (this.state.progress === "complete") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
<p>{ _t('Your password has been reset') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={ _t('Return to login screen') } />
|
||||
value={_t('Return to login screen')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!config.disable_custom_urls) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0} />
|
||||
);
|
||||
}
|
||||
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
@@ -199,35 +208,25 @@ module.exports = React.createClass({
|
||||
name="reset_email" // define a name so browser's password autofill gets less confused
|
||||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
placeholder={ _t('Email address') } autoFocus />
|
||||
placeholder={_t('Email address')} autoFocus />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password"
|
||||
value={this.state.password}
|
||||
onChange={this.onInputChanged.bind(this, "password")}
|
||||
placeholder={ _t('New password') } />
|
||||
placeholder={_t('New password')} />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
name="reset_password_confirm"
|
||||
value={this.state.password2}
|
||||
onChange={this.onInputChanged.bind(this, "password2")}
|
||||
placeholder={ _t('Confirm your new password') } />
|
||||
placeholder={_t('Confirm your new password')} />
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="submit" value={ _t('Send Reset Email') } />
|
||||
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
|
||||
</form>
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
delayTimeMs={0}/>
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
{ serverConfigSection }
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{_t('Return to login screen')}
|
||||
{ _t('Return to login screen') }
|
||||
</a>
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
@@ -240,12 +239,12 @@ module.exports = React.createClass({
|
||||
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
{resetPasswordJsx}
|
||||
{ resetPasswordJsx }
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,12 +18,13 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
@@ -76,6 +77,14 @@ module.exports = React.createClass({
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
|
||||
// map from login step type to a function which will render a control
|
||||
// letting you do that login type
|
||||
this._stepRendererMap = {
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
'm.login.cas': this._renderCasStep,
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
},
|
||||
|
||||
@@ -95,7 +104,7 @@ module.exports = React.createClass({
|
||||
).then((data) => {
|
||||
this.props.onLoggedIn(data);
|
||||
}, (error) => {
|
||||
if(this._unmounted) {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
let errorText;
|
||||
@@ -105,7 +114,22 @@ module.exports = React.createClass({
|
||||
if (error.httpStatus == 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
errorText = _t('Incorrect username and/or password.');
|
||||
if (SdkConfig.get().disable_custom_urls) {
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
<div className="mx_Login_smallError">
|
||||
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
|
||||
{
|
||||
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''),
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
errorText = _t('Incorrect username and/or password.');
|
||||
}
|
||||
} else {
|
||||
// other errors, not specific to doing a password login
|
||||
errorText = this._errorTextFromError(error);
|
||||
@@ -120,7 +144,7 @@ module.exports = React.createClass({
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if(this._unmounted) {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
@@ -134,7 +158,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_onLoginAsGuestClick: function() {
|
||||
var self = this;
|
||||
const self = this;
|
||||
self.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
@@ -156,7 +180,7 @@ module.exports = React.createClass({
|
||||
});
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
@@ -183,8 +207,8 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
var self = this;
|
||||
let newState = {
|
||||
const self = this;
|
||||
const newState = {
|
||||
errorText: null, // reset err messages
|
||||
};
|
||||
if (config.hsUrl !== undefined) {
|
||||
@@ -199,13 +223,13 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_initLoginLogic: function(hsUrl, isUrl) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
|
||||
var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
const fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
this._loginLogic = loginLogic;
|
||||
@@ -217,13 +241,29 @@ module.exports = React.createClass({
|
||||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
loginLogic.getFlows().then(function(flows) {
|
||||
// old behaviour was to always use the first flow without presenting
|
||||
// options. This works in most cases (we don't have a UI for multiple
|
||||
// logins so let's skip that for now).
|
||||
loginLogic.chooseFlow(0);
|
||||
self.setState({
|
||||
currentFlow: self._getCurrentFlowStep(),
|
||||
loginLogic.getFlows().then((flows) => {
|
||||
// look for a flow where we understand all of the steps.
|
||||
for (let i = 0; i < flows.length; i++ ) {
|
||||
if (!this._isSupportedFlow(flows[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we just pick the first flow where we support all the
|
||||
// steps. (we don't have a UI for multiple logins so let's skip
|
||||
// that for now).
|
||||
loginLogic.chooseFlow(i);
|
||||
this.setState({
|
||||
currentFlow: this._getCurrentFlowStep(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// we got to the end of the list without finding a suitable
|
||||
// flow.
|
||||
this.setState({
|
||||
errorText: _t(
|
||||
"This homeserver doesn't offer any login flows which are " +
|
||||
"supported by this client.",
|
||||
),
|
||||
});
|
||||
}, function(err) {
|
||||
self.setState({
|
||||
@@ -237,6 +277,16 @@ module.exports = React.createClass({
|
||||
}).done();
|
||||
},
|
||||
|
||||
_isSupportedFlow: function(flow) {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login and loginLogic doesn't support it so we can ignore it.
|
||||
if (!this._stepRendererMap[flow.type]) {
|
||||
console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_getCurrentFlowStep: function() {
|
||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||
},
|
||||
@@ -256,18 +306,20 @@ module.exports = React.createClass({
|
||||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
{
|
||||
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; }
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
||||
) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{ _tJsx("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
/<a>(.*?)<\/a>/,
|
||||
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; }
|
||||
)}
|
||||
{
|
||||
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
||||
) }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
@@ -276,42 +328,47 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
componentForStep: function(step) {
|
||||
switch (step) {
|
||||
case 'm.login.password':
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
/>
|
||||
);
|
||||
case 'm.login.cas':
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
default:
|
||||
if (!step) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step})
|
||||
</div>
|
||||
);
|
||||
if (!step) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepRenderer = this._stepRendererMap[step];
|
||||
|
||||
if (stepRenderer) {
|
||||
return stepRenderer();
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
_renderPasswordStep: function() {
|
||||
const PasswordLogin = sdk.getComponent('login.PasswordLogin');
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
hsUrl={this.state.enteredHomeserverUrl}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
_renderCasStep: function() {
|
||||
const CasLogin = sdk.getComponent('login.CasLogin');
|
||||
return (
|
||||
<CasLogin onSubmit={this.onCasLogin} />
|
||||
);
|
||||
},
|
||||
|
||||
_onLanguageChange: function(newLang) {
|
||||
if(languageHandler.getCurrentLanguage() !== newLang) {
|
||||
UserSettingsStore.setLocalSetting('language', newLang);
|
||||
if (languageHandler.getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
},
|
||||
@@ -328,57 +385,83 @@ module.exports = React.createClass({
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
const LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||
const LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||
const ServerConfig = sdk.getComponent("login.ServerConfig");
|
||||
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||
|
||||
var loginAsGuestJsx;
|
||||
let loginAsGuestJsx;
|
||||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||
{ _t('Login as guest')}
|
||||
{ _t('Login as guest') }
|
||||
</a>;
|
||||
}
|
||||
|
||||
var returnToAppJsx;
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app')}
|
||||
{ _t('Return to app') }
|
||||
</a>;
|
||||
}
|
||||
*/
|
||||
|
||||
let serverConfig;
|
||||
let header;
|
||||
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000} />;
|
||||
}
|
||||
|
||||
// FIXME: remove status.im theme tweaks
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
let errorTextSection;
|
||||
if (this.state.errorText) {
|
||||
errorTextSection = (
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div>
|
||||
<h2>{ _t('Sign in')}
|
||||
{ loader }
|
||||
</h2>
|
||||
{ header }
|
||||
{ errorTextSection }
|
||||
{ this.componentForStep(this.state.currentFlow) }
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}/>
|
||||
<div className="mx_Login_error">
|
||||
{ this.state.errorText }
|
||||
</div>
|
||||
{ serverConfig }
|
||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account')}
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
{ returnToAppJsx }
|
||||
{ this._renderLanguageSetting() }
|
||||
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,14 +25,14 @@ module.exports = React.createClass({
|
||||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
onComplete: React.PropTypes.func.isRequired
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
avatarUrl: null,
|
||||
errorString: null,
|
||||
busy: false
|
||||
busy: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -40,28 +40,29 @@ module.exports = React.createClass({
|
||||
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||
// the URL to be passed to you (because it's also used for room avatars).
|
||||
var cli = MatrixClientPeg.get();
|
||||
const cli = MatrixClientPeg.get();
|
||||
this.setState({busy: true});
|
||||
var self = this;
|
||||
const self = this;
|
||||
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||
self.setState({
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
errorString: _t("Failed to fetch avatar URL"),
|
||||
busy: false
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader />
|
||||
<div className="mx_Login_profile">
|
||||
@@ -71,10 +72,10 @@ module.exports = React.createClass({
|
||||
<ChangeAvatar
|
||||
initialAvatarUrl={this.state.avatarUrl} />
|
||||
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
|
||||
{this.state.errorString}
|
||||
{ this.state.errorString }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
@@ -57,7 +59,7 @@ module.exports = React.createClass({
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onCancelClick: React.PropTypes.func
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@@ -121,7 +123,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onServerConfigChange: function(config) {
|
||||
let newState = {};
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.hsUrl = config.hsUrl;
|
||||
}
|
||||
@@ -195,7 +197,7 @@ module.exports = React.createClass({
|
||||
|
||||
this._rtsClient.getTeam(teamToken).then((team) => {
|
||||
console.log(
|
||||
`User successfully registered with team ${team.name}`
|
||||
`User successfully registered with team ${team.name}`,
|
||||
);
|
||||
if (!team.rooms) {
|
||||
return;
|
||||
@@ -223,7 +225,7 @@ module.exports = React.createClass({
|
||||
deviceId: response.device_id,
|
||||
homeserverUrl: this._matrixClient.getHomeserverUrl(),
|
||||
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
|
||||
accessToken: response.access_token
|
||||
accessToken: response.access_token,
|
||||
}, teamToken);
|
||||
}).then((cli) => {
|
||||
return this._setupPushers(cli);
|
||||
@@ -253,7 +255,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onFormValidationFailed: function(errCode) {
|
||||
var errMsg;
|
||||
let errMsg;
|
||||
switch (errCode) {
|
||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||
errMsg = _t('Missing password.');
|
||||
@@ -282,7 +284,7 @@ module.exports = React.createClass({
|
||||
break;
|
||||
}
|
||||
this.setState({
|
||||
errorText: errMsg
|
||||
errorText: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -316,16 +318,19 @@ module.exports = React.createClass({
|
||||
emailAddress: this.state.formVals.email,
|
||||
phoneCountry: this.state.formVals.phoneCountry,
|
||||
phoneNumber: this.state.formVals.phoneNumber,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||
const LoginFooter = sdk.getComponent('login.LoginFooter');
|
||||
const LoginPage = sdk.getComponent('login.LoginPage');
|
||||
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const ServerConfig = sdk.getComponent('views.login.ServerConfig');
|
||||
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
|
||||
let registerBody;
|
||||
if (this.state.doingUIAuth) {
|
||||
registerBody = (
|
||||
@@ -344,9 +349,19 @@ module.exports = React.createClass({
|
||||
} else if (this.state.busy || this.state.teamServerBusy) {
|
||||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let errorSection;
|
||||
if (this.state.errorText) {
|
||||
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>;
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
);
|
||||
}
|
||||
registerBody = (
|
||||
<div>
|
||||
@@ -362,30 +377,46 @@ module.exports = React.createClass({
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onTeamSelected={this.onTeamSelected}
|
||||
/>
|
||||
{errorSection}
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
defaultHsUrl={this.props.defaultHsUrl}
|
||||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={1000}
|
||||
/>
|
||||
{ serverConfigSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{_t('Return to app')}
|
||||
{ _t('Return to app') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
let header;
|
||||
let errorText;
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
if (theme === 'status' && this.state.errorText) {
|
||||
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
} else {
|
||||
header = <h2>{ _t('Create an account') }</h2>;
|
||||
if (this.state.errorText) {
|
||||
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let signIn;
|
||||
if (!this.state.doingUIAuth) {
|
||||
signIn = (
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_Login">
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
<LoginHeader
|
||||
icon={this.state.teamSelected ?
|
||||
@@ -393,15 +424,14 @@ module.exports = React.createClass({
|
||||
this.state.teamSelected.domain + "/icon.png" :
|
||||
null}
|
||||
/>
|
||||
<h2>{_t('Create an account')}</h2>
|
||||
{registerBody}
|
||||
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||
{_t('I already have an account')}
|
||||
</a>
|
||||
{returnToAppJsx}
|
||||
{ header }
|
||||
{ registerBody }
|
||||
{ signIn }
|
||||
{ errorText }
|
||||
{ returnToAppJsx }
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
</LoginPage>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var AvatarLogic = require("../../../Avatar");
|
||||
import React from 'react';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
@@ -34,7 +32,7 @@ module.exports = React.createClass({
|
||||
height: React.PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -42,7 +40,7 @@ module.exports = React.createClass({
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -52,15 +50,14 @@ module.exports = React.createClass({
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
var newState = this._getState(nextProps);
|
||||
var newImageUrls = newState.imageUrls;
|
||||
var oldImageUrls = this.state.imageUrls;
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (var i = 0; i < newImageUrls.length; i++) {
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
@@ -73,31 +70,31 @@ module.exports = React.createClass({
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
||||
var urls = props.urls || [];
|
||||
const urls = props.urls || [];
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
}
|
||||
|
||||
var defaultImageUrl = null;
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
|
||||
onError: function(ev) {
|
||||
var nextIndex = this.state.urlsIndex + 1;
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -111,32 +108,32 @@ module.exports = React.createClass({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var idx = 0;
|
||||
var initial = name[0];
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
// string.codePointAt(0) would do this, but that isn't supported by
|
||||
// some browsers (notably PhantomJS).
|
||||
var chars = 1;
|
||||
var first = name.charCodeAt(idx);
|
||||
let chars = 1;
|
||||
const first = name.charCodeAt(idx);
|
||||
|
||||
// check if it’s the start of a surrogate pair
|
||||
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
|
||||
var second = name.charCodeAt(idx+1);
|
||||
const second = name.charCodeAt(idx+1);
|
||||
if (second >= 0xDC00 && second <= 0xDFFF) {
|
||||
chars++;
|
||||
}
|
||||
}
|
||||
|
||||
var firstChar = name.substring(idx, idx+chars);
|
||||
const firstChar = name.substring(idx, idx+chars);
|
||||
return firstChar.toUpperCase();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
@@ -152,7 +149,7 @@ module.exports = React.createClass({
|
||||
width: width + "px",
|
||||
lineHeight: height + "px" }}
|
||||
>
|
||||
{initialLetter}
|
||||
{ initialLetter }
|
||||
</EmojiText>
|
||||
);
|
||||
const imgNode = (
|
||||
@@ -165,15 +162,15 @@ module.exports = React.createClass({
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} {...otherProps}
|
||||
>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" {...otherProps}>
|
||||
{textNode}
|
||||
{imgNode}
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -198,5 +195,5 @@ module.exports = React.createClass({
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,10 +24,12 @@ export default React.createClass({
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string,
|
||||
groupName: PropTypes.string,
|
||||
groupAvatarUrl: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -52,11 +54,11 @@ export default React.createClass({
|
||||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||
const {groupId, groupAvatarUrl, ...otherProps} = this.props;
|
||||
const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar
|
||||
name={this.props.groupId[1]}
|
||||
name={groupName || this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
|
||||
@@ -16,9 +16,9 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
const React = require('react');
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
@@ -63,14 +63,14 @@ module.exports = React.createClass({
|
||||
imageUrl: Avatar.avatarUrlForMember(props.member,
|
||||
props.width,
|
||||
props.height,
|
||||
props.resizeMethod)
|
||||
props.resizeMethod),
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
let {member, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||
|
||||
if (viewUserOnClick) {
|
||||
onClick = () => {
|
||||
@@ -83,7 +83,7 @@ module.exports = React.createClass({
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick}/>
|
||||
idName={member.userId} url={this.state.imageUrl} onClick={onClick} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
168
src/components/views/avatars/MemberPresenceAvatar.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import * as sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Presence from "../../../Presence";
|
||||
import dispatcher from "../../../dispatcher";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// This is an avatar with presence information and controls on it.
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberPresenceAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
let presenceState = null;
|
||||
let presenceMessage = null;
|
||||
|
||||
// RoomMembers do not necessarily have a user.
|
||||
if (this.props.member.user) {
|
||||
presenceState = this.props.member.user.presence;
|
||||
presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
}
|
||||
|
||||
return {
|
||||
status: presenceState,
|
||||
message: presenceMessage,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
|
||||
this.dispatcherRef = dispatcher.register(this.onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
|
||||
}
|
||||
dispatcher.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action !== "self_presence_updated") return;
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: payload.statusInfo.presence,
|
||||
message: payload.statusInfo.status_msg,
|
||||
});
|
||||
},
|
||||
|
||||
onUserPresence: function(event, user) {
|
||||
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.setState({
|
||||
status: user.presence,
|
||||
message: user.presenceStatusMsg,
|
||||
});
|
||||
},
|
||||
|
||||
onStatusChange: function(newStatus) {
|
||||
Presence.stopMaintainingStatus();
|
||||
if (newStatus === "online") {
|
||||
Presence.setState(newStatus);
|
||||
} else Presence.setState(newStatus, null, true);
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
|
||||
|
||||
ContextualMenu.createMenu(PresenceContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
chevronFace: 'bottom',
|
||||
left: x,
|
||||
top: y,
|
||||
menuWidth: 125,
|
||||
currentStatus: this.state.status,
|
||||
onChange: this.onStatusChange,
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
// XXX NB the following assumes that user is non-null, which is not valid
|
||||
// const presenceState = this.props.member.user.presence;
|
||||
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
// const presenceLastTs = this.props.member.user.lastPresenceTs;
|
||||
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
// const presenceMessage = this.props.member.user.presenceStatusMsg;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
|
||||
|
||||
let onClickFn = null;
|
||||
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
|
||||
onClickFn = this.onClick;
|
||||
}
|
||||
|
||||
const avatarNode = (
|
||||
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
|
||||
resizeMethod={this.props.resizeMethod} />
|
||||
);
|
||||
let statusNode = (
|
||||
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
|
||||
);
|
||||
|
||||
// LABS: Disable presence management functions for now
|
||||
// Also disable the presence information if there's no status information
|
||||
if (!SettingsStore.isFeatureEnabled("feature_presence_management") || !this.state.status) {
|
||||
statusNode = null;
|
||||
onClickFn = null;
|
||||
}
|
||||
|
||||
let avatar = (
|
||||
<div className="mx_MemberPresenceAvatar">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</div>
|
||||
);
|
||||
if (onClickFn) {
|
||||
avatar = (
|
||||
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
|
||||
{ avatarNode }
|
||||
{ statusNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
return avatar;
|
||||
},
|
||||
});
|
||||
@@ -13,11 +13,10 @@ 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.
|
||||
*/
|
||||
var React = require('react');
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
import React from "react";
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from "../../../index";
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
@@ -30,7 +29,7 @@ module.exports = React.createClass({
|
||||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
resizeMethod: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -44,13 +43,13 @@ module.exports = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urls: this.getImageUrls(this.props)
|
||||
urls: this.getImageUrls(this.props),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
urls: this.getImageUrls(newProps)
|
||||
urls: this.getImageUrls(newProps),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -61,11 +60,10 @@ module.exports = React.createClass({
|
||||
props.oobData.avatarUrl,
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod
|
||||
props.resizeMethod,
|
||||
), // highest priority
|
||||
this.getRoomAvatarUrl(props),
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
this.getOneToOneAvatar(props), // lowest priority
|
||||
].filter(function(url) {
|
||||
return (url != null && url != "");
|
||||
});
|
||||
@@ -79,17 +77,17 @@ module.exports = React.createClass({
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
getOneToOneAvatar: function(props) {
|
||||
if (!props.room) return null;
|
||||
|
||||
var mlist = props.room.currentState.members;
|
||||
var userIds = [];
|
||||
const mlist = props.room.currentState.members;
|
||||
const userIds = [];
|
||||
// for .. in optimisation to return early if there are >2 keys
|
||||
for (var uid in mlist) {
|
||||
for (const uid in mlist) {
|
||||
if (mlist.hasOwnProperty(uid)) {
|
||||
userIds.push(uid);
|
||||
}
|
||||
@@ -99,7 +97,7 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
let theOtherGuy = null;
|
||||
if (mlist[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = mlist[userIds[1]];
|
||||
} else {
|
||||
@@ -110,7 +108,7 @@ module.exports = React.createClass({
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else if (userIds.length == 1) {
|
||||
return mlist[userIds[0]].getAvatarUrl(
|
||||
@@ -118,37 +116,24 @@ module.exports = React.createClass({
|
||||
Math.floor(props.width * window.devicePixelRatio),
|
||||
Math.floor(props.height * window.devicePixelRatio),
|
||||
props.resizeMethod,
|
||||
false
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getFallbackAvatar: function(props) {
|
||||
let roomId = null;
|
||||
if (props.oobData && props.oobData.roomId) {
|
||||
roomId = this.props.oobData.roomId;
|
||||
} else if (props.room) {
|
||||
roomId = props.room.roomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Avatar.defaultAvatarUrlForString(roomId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
|
||||
var {room, oobData, ...otherProps} = this.props;
|
||||
const {room, oobData, ...otherProps} = this.props;
|
||||
|
||||
var roomName = room ? room.name : oobData.name;
|
||||
const roomName = room ? room.name : oobData.name;
|
||||
|
||||
return (
|
||||
<BaseAvatar {...otherProps} name={roomName}
|
||||
idName={room ? room.roomId : null}
|
||||
urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports = React.createClass({
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button>
|
||||
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -16,10 +16,10 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
var Presets = {
|
||||
const Presets = {
|
||||
PrivateChat: "private_chat",
|
||||
PublicChat: "public_chat",
|
||||
Custom: "custom",
|
||||
@@ -29,7 +29,7 @@ module.exports = React.createClass({
|
||||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
preset: React.PropTypes.string
|
||||
preset: React.PropTypes.string,
|
||||
},
|
||||
|
||||
Presets: Presets,
|
||||
@@ -47,10 +47,10 @@ module.exports = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option>
|
||||
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option>
|
||||
<option value={this.Presets.Custom}>{_t("Custom")}</option>
|
||||
<option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
|
||||
<option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
|
||||
<option value={this.Presets.Custom}>{ _t("Custom") }</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
const React = require('react');
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
@@ -35,10 +35,10 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
getAliasLocalpart: function() {
|
||||
var room_alias = this.props.alias;
|
||||
let room_alias = this.props.alias;
|
||||
|
||||
if (room_alias && this.props.homeserver) {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
||||
room_alias = room_alias.slice(1, -suffix.length);
|
||||
}
|
||||
@@ -52,22 +52,22 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
var target = ev.target;
|
||||
var curr_val = ev.target.value;
|
||||
const target = ev.target;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "") {
|
||||
var self = this;
|
||||
const self = this;
|
||||
setTimeout(function() {
|
||||
target.value = "#:" + self.props.homeserver;
|
||||
target.setSelectionRange(1, 1);
|
||||
}, 0);
|
||||
} else {
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
setTimeout(function() {
|
||||
target.setSelectionRange(
|
||||
curr_val.startsWith("#") ? 1 : 0,
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length
|
||||
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length,
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onBlur: function(ev) {
|
||||
var curr_val = ev.target.value;
|
||||
const curr_val = ev.target.value;
|
||||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "#:" + this.props.homeserver) {
|
||||
@@ -84,8 +84,8 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
if (curr_val != "") {
|
||||
var new_val = ev.target.value;
|
||||
var suffix = ":" + this.props.homeserver;
|
||||
let new_val = ev.target.value;
|
||||
const suffix = ":" + this.props.homeserver;
|
||||
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
||||
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
||||
ev.target.value = new_val;
|
||||
@@ -97,7 +97,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias}/>
|
||||
value={this.props.alias} />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,16 +23,19 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "UserPickerDialog",
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
propTypes: {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.node,
|
||||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode: PropTypes.node,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
@@ -40,6 +43,12 @@ module.exports = React.createClass({
|
||||
focus: PropTypes.bool,
|
||||
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
groupId: PropTypes.string,
|
||||
// The type of entity to search for. Default: 'user'.
|
||||
pickerType: PropTypes.oneOf(['user', 'room']),
|
||||
// Whether the current user should be included in the addresses returned. Only
|
||||
// applicable when pickerType is `user`. Default: false.
|
||||
includeSelf: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@@ -47,6 +56,8 @@ module.exports = React.createClass({
|
||||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -140,10 +151,22 @@ module.exports = React.createClass({
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupSearch(query);
|
||||
} else if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
} else if (this.props.pickerType === 'room') {
|
||||
if (this.props.groupId) {
|
||||
this._doNaiveGroupRoomSearch(query);
|
||||
} else {
|
||||
this._doRoomSearch(query);
|
||||
}
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
console.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
@@ -185,6 +208,120 @@ module.exports = React.createClass({
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_doNaiveGroupSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||
const results = [];
|
||||
resp.chunk.forEach((u) => {
|
||||
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(userIdMatch || displayNameMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
user_id: u.user_id,
|
||||
avatar_url: u.avatar_url,
|
||||
display_name: u.displayname,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst searching group rooms: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_doNaiveGroupRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const groupStore = GroupStoreCache.getGroupStore(this.props.groupId);
|
||||
const results = [];
|
||||
groupStore.getGroupRooms().forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
room_id: r.room_id,
|
||||
avatar_url: r.avatar_url,
|
||||
name: r.name || r.canonical_alias,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doRoomSearch: function(query) {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const results = [];
|
||||
rooms.forEach((room) => {
|
||||
let rank = Infinity;
|
||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
let aliasMatch = false;
|
||||
let shortestMatchingAliasLength = Infinity;
|
||||
aliases.forEach((alias) => {
|
||||
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||
aliasMatch = true;
|
||||
if (shortestMatchingAliasLength > alias.length) {
|
||||
shortestMatchingAliasLength = alias.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(nameMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aliasMatch) {
|
||||
// A shorter matching alias will give a better rank
|
||||
rank = shortestMatchingAliasLength;
|
||||
}
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
rank,
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by rank ascending (a high rank being less relevant)
|
||||
const sortedResults = results.sort((a, b) => {
|
||||
return a.rank - b.rank;
|
||||
});
|
||||
|
||||
this._processResults(sortedResults, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
},
|
||||
|
||||
_doUserDirectorySearch: function(query) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
@@ -245,17 +382,30 @@ module.exports = React.createClass({
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
@@ -291,16 +441,23 @@ module.exports = React.createClass({
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
if (addrType == null) {
|
||||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx') {
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType == 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.avatarMxc = room.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
@@ -360,7 +517,12 @@ module.exports = React.createClass({
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -382,23 +544,37 @@ module.exports = React.createClass({
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
let tryUsing = '';
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||
return {
|
||||
'mx-user-id': _t("Matrix ID"),
|
||||
'mx-room-id': _t("Matrix Room ID"),
|
||||
'email': _t("email address"),
|
||||
}[t];
|
||||
});
|
||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
});
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ tryUsing }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
addressList={this.state.queryList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -406,7 +582,7 @@ module.exports = React.createClass({
|
||||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
{ this.props.title }
|
||||
</div>
|
||||
<AccessibleButton className="mx_ChatInviteDialog_cancel"
|
||||
onClick={this.onCancel} >
|
||||
@@ -419,10 +595,11 @@ module.exports = React.createClass({
|
||||
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{this.props.button}
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
width={48} height={48}
|
||||
/>
|
||||
<div className="mx_ChatCreateOrReuseDialog_profile_name">
|
||||
{this.state.profile.displayName || this.props.userId}
|
||||
{ this.state.profile.displayName || this.props.userId }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@@ -177,7 +177,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={ this.props.onFinished.bind(false) }
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
title={title}
|
||||
>
|
||||
{ content }
|
||||
|
||||
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import classnames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
/*
|
||||
@@ -24,51 +23,17 @@ import { _t } from '../../../languageHandler';
|
||||
*/
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmRedactDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
danger: false,
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
const title = _t("Confirm Removal");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': false,
|
||||
});
|
||||
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass} onClick={this.onOk}>
|
||||
{_t("Remove")}
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<QuestionDialog onFinished={this.props.onFinished}
|
||||
title={_t("Confirm Removal")}
|
||||
description={
|
||||
_t("Are you sure you wish to remove (delete) this event? " +
|
||||
"Note that if you delete a room name or topic change, it could undo the change.")}
|
||||
button={_t("Remove")}>
|
||||
</QuestionDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,9 +15,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
* A dialog for confirming an operation on another user.
|
||||
@@ -30,8 +32,14 @@ import classnames from 'classnames';
|
||||
export default React.createClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: React.PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
@@ -69,8 +77,8 @@ export default React.createClass({
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
@@ -83,7 +91,7 @@ export default React.createClass({
|
||||
<form onSubmit={this.onOk}>
|
||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||
ref={this._collectReasonField}
|
||||
placeholder={ _t("Reason") }
|
||||
placeholder={_t("Reason")}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</form>
|
||||
@@ -91,24 +99,39 @@ export default React.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
let avatar;
|
||||
let name;
|
||||
let userId;
|
||||
if (this.props.member) {
|
||||
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl ?
|
||||
this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
title={title}
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_ConfirmUserActionDialog_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
{ avatar }
|
||||
</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{this.props.member.name}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{this.props.member.userId}</div>
|
||||
<div className="mx_ConfirmUserActionDialog_name">{ name }</div>
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{reasonBox}
|
||||
{ reasonBox }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{this.props.action}
|
||||
{ this.props.action }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
|
||||
@@ -21,10 +21,6 @@ import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// We match fairly liberally and leave it up to the server to reject if
|
||||
// there are invalid characters etc.
|
||||
const GROUP_REGEX = /^\+(.*?):(.*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
@@ -58,22 +54,9 @@ export default React.createClass({
|
||||
},
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
let error = null;
|
||||
if (parsedGroupId === null) {
|
||||
error = _t(
|
||||
"Group IDs must be of the form +localpart:%(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
} else {
|
||||
const domain = parsedGroupId[1];
|
||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||
error = _t(
|
||||
"It is currently only possible to create groups on your own home server: "+
|
||||
"use a group ID ending with %(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
}
|
||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
@@ -86,19 +69,19 @@ export default React.createClass({
|
||||
|
||||
if (this._checkGroupId()) return;
|
||||
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
const profile = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({creating: true});
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: parsedGroupId[0],
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
@@ -112,22 +95,6 @@ export default React.createClass({
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a string that may be a group ID
|
||||
* If the string is a valid group ID, return a list of [localpart, domain],
|
||||
* otherwise return null.
|
||||
*
|
||||
* @param {string} groupId The ID of the group
|
||||
* @return {string[]} array of localpart, domain
|
||||
*/
|
||||
_parseGroupId: function(groupId) {
|
||||
const matches = GROUP_REGEX.exec(this.state.groupId);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
@@ -142,21 +109,21 @@ export default React.createClass({
|
||||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{_t('Room creation failed')}</div>
|
||||
<div>{this.state.createError.message}</div>
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Group')}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{_t('Group Name')}</label>
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
@@ -169,22 +136,27 @@ export default React.createClass({
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{_t('Group ID')}</label>
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
size="64"
|
||||
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size="32"
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{this.state.groupIdError}
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{createErrorNode}
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onCancel}>
|
||||
|
||||
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
81
src/components/views/dialogs/CreateRoomDialog.js
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
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 sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
},
|
||||
|
||||
onOk: function() {
|
||||
this.props.onFinished(true, this.refs.textinput.value, this.refs.checkbox.checked);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
title={_t('Create Room')}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<details className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
|
||||
<div>
|
||||
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
|
||||
<label htmlFor="checkbox">
|
||||
{ _t('Block users on other matrix homeservers from joining this room') }
|
||||
<br />
|
||||
({ _t('This setting cannot be changed later!') })
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t('Cancel') }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ _t('Create Room') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -83,7 +83,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
let error = null;
|
||||
if (this.state.errStr) {
|
||||
error = <div className="error">
|
||||
{this.state.errStr}
|
||||
{ this.state.errStr }
|
||||
</div>;
|
||||
passwordBoxClass = 'error';
|
||||
}
|
||||
@@ -94,30 +94,30 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
let cancelButton = null;
|
||||
if (!this.state.busy) {
|
||||
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
{_t("Deactivate Account")}
|
||||
{ _t("Deactivate Account") }
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("This will make your account permanently unusable. You will not be able to re-register the same user ID.")}</p>
|
||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||
|
||||
<p>{_t("This action is irreversible.")}</p>
|
||||
<p>{ _t("This action is irreversible.") }</p>
|
||||
|
||||
<p>{_t("To continue, please enter your password.")}</p>
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
|
||||
<p>{_t("Password")}:</p>
|
||||
<p>{ _t("Password") }:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{error}
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button
|
||||
@@ -125,10 +125,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
onClick={this._onOk}
|
||||
disabled={!okEnabled}
|
||||
>
|
||||
{okLabel}
|
||||
{ okLabel }
|
||||
</button>
|
||||
|
||||
{cancelButton}
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -28,25 +28,25 @@ export default function DeviceVerifyDialog(props) {
|
||||
const body = (
|
||||
<div>
|
||||
<p>
|
||||
{_t("To verify that this device can be trusted, please contact its " +
|
||||
{ _t("To verify that this device can be trusted, please contact its " +
|
||||
"owner using some other means (e.g. in person or a phone call) " +
|
||||
"and ask them whether the key they see in their User Settings " +
|
||||
"for this device matches the key below:")}
|
||||
"for this device matches the key below:") }
|
||||
</p>
|
||||
<div className="mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device name")}:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{_t("Device ID")}:</label> <span><code>{ props.device.deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key")}:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
<li><label>{ _t("Device name") }:</label> <span>{ props.device.getDisplayName() }</span></li>
|
||||
<li><label>{ _t("Device ID") }:</label> <span><code>{ props.device.deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
{_t("If it matches, press the verify button below. " +
|
||||
{ _t("If it matches, press the verify button below. " +
|
||||
"If it doesn't, then someone else is intercepting this device " +
|
||||
"and you probably want to press the blacklist button instead.")}
|
||||
"and you probably want to press the blacklist button instead.") }
|
||||
</p>
|
||||
<p>
|
||||
{_t("In future this verification process will be more sophisticated.")}
|
||||
{ _t("In future this verification process will be more sophisticated.") }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -63,11 +63,11 @@ export default React.createClass({
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={this.props.title || _t('Error')}>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description || _t('An error has occurred.')}
|
||||
{ this.props.description || _t('An error has occurred.') }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button ref="button" className="mx_Dialog_primary" onClick={this.props.onFinished}>
|
||||
{this.props.button || _t('OK')}
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authError: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_onAuthFinished: function(success, result) {
|
||||
@@ -73,12 +73,12 @@ export default React.createClass({
|
||||
if (this.state.authError) {
|
||||
content = (
|
||||
<div>
|
||||
<div>{this.state.authError.message || this.state.authError.toString()}</div>
|
||||
<div>{ this.state.authError.message || this.state.authError.toString() }</div>
|
||||
<br />
|
||||
<AccessibleButton onClick={this._onDismissClick}
|
||||
className="mx_UserSettings_button"
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
{ _t("Dismiss") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
@@ -100,7 +100,7 @@ export default React.createClass({
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
|
||||
/**
|
||||
* Dialog which asks the user whether they want to share their keys with
|
||||
@@ -54,7 +54,7 @@ export default React.createClass({
|
||||
|
||||
const deviceInfo = r[userId][deviceId];
|
||||
|
||||
if(!deviceInfo) {
|
||||
if (!deviceInfo) {
|
||||
console.warn(`No details found for device ${userId}:${deviceId}`);
|
||||
|
||||
this.props.onFinished(false);
|
||||
@@ -116,27 +116,27 @@ export default React.createClass({
|
||||
|
||||
let text;
|
||||
if (this.state.wasNewDevice) {
|
||||
text = "You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.";
|
||||
text = _td("You added a new device '%(displayName)s', which is"
|
||||
+ " requesting encryption keys.");
|
||||
} else {
|
||||
text = "Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.";
|
||||
text = _td("Your unverified device '%(displayName)s' is requesting"
|
||||
+ " encryption keys.");
|
||||
}
|
||||
text = _t(text, {displayName: displayName});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{text}</p>
|
||||
<p>{ text }</p>
|
||||
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this._onVerifyClicked}>
|
||||
{_t('Start verification')}
|
||||
{ _t('Start verification') }
|
||||
</button>
|
||||
<button onClick={this._onShareClicked}>
|
||||
{_t('Share without verifying')}
|
||||
{ _t('Share without verifying') }
|
||||
</button>
|
||||
<button onClick={this._onIgnoreClicked}>
|
||||
{_t('Ignore request')}
|
||||
{ _t('Ignore request') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,7 +154,7 @@ export default React.createClass({
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>{_t('Loading device info...')}</p>
|
||||
<p>{ _t('Loading device info...') }</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
@@ -165,7 +165,7 @@ export default React.createClass({
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Encryption key request')}
|
||||
>
|
||||
{content}
|
||||
{ content }
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +18,7 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
@@ -25,6 +27,7 @@ export default React.createClass({
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
danger: React.PropTypes.bool,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
@@ -36,6 +39,7 @@ export default React.createClass({
|
||||
extraButtons: null,
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -51,23 +55,27 @@ export default React.createClass({
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{_t("Cancel")}
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
) : null;
|
||||
const buttonClasses = classnames({
|
||||
mx_Dialog_primary: true,
|
||||
danger: this.props.danger,
|
||||
});
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={ this.onOk }
|
||||
onEnterPressed={this.onOk}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{this.props.description}
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{this.props.button || _t('OK')}
|
||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
{this.props.extraButtons}
|
||||
{cancelButton}
|
||||
{ this.props.extraButtons }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
export default React.createClass({
|
||||
@@ -45,10 +45,11 @@ export default React.createClass({
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugreport = (
|
||||
<p>
|
||||
{_tJsx(
|
||||
{ _t(
|
||||
"Otherwise, <a>click here</a> to send a bug report.",
|
||||
/<a>(.*?)<\/a>/, (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{sub}</a>,
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => <a onClick={this._sendBugReport} key="bugreport" href='#'>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -57,19 +58,19 @@ export default React.createClass({
|
||||
<BaseDialog className="mx_ErrorDialog" onFinished={this.props.onFinished}
|
||||
title={_t('Unable to restore session')}>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{_t("We encountered an error trying to restore your previous session. If " +
|
||||
<p>{ _t("We encountered an error trying to restore your previous session. If " +
|
||||
"you continue, you will need to log in again, and encrypted chat " +
|
||||
"history will be unreadable.")}</p>
|
||||
"history will be unreadable.") }</p>
|
||||
|
||||
<p>{_t("If you have previously used a more recent version of Riot, your session " +
|
||||
<p>{ _t("If you have previously used a more recent version of Riot, your session " +
|
||||
"may be incompatible with this version. Close this window and return " +
|
||||
"to the more recent version.")}</p>
|
||||
"to the more recent version.") }</p>
|
||||
|
||||
{bugreport}
|
||||
{ bugreport }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{_t("Continue anyway")}
|
||||
{ _t("Continue anyway") }
|
||||
</button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -130,10 +130,10 @@ export default React.createClass({
|
||||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
placeholder={ _t("Email address") }
|
||||
placeholder={_t("Email address")}
|
||||
placeholderClassName="mx_SetEmailDialog_email_input_placeholder"
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this.onEmailAddressChanged } />;
|
||||
blurToCancel={false}
|
||||
onValueChanged={this.onEmailAddressChanged} />;
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_SetEmailDialog"
|
||||
|
||||
@@ -20,8 +20,8 @@ import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
import KeyCode from '../../../KeyCode';
|
||||
import { _t, _tJsx } from '../../../languageHandler';
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// The amount of time to wait for further changes to the input username before
|
||||
// sending a request to the server
|
||||
@@ -226,7 +226,7 @@ export default React.createClass({
|
||||
let usernameIndicator = null;
|
||||
let usernameBusyIndicator = null;
|
||||
if (this.state.usernameBusy) {
|
||||
usernameBusyIndicator = <Spinner w="24" h="24"/>;
|
||||
usernameBusyIndicator = <Spinner w="24" h="24" />;
|
||||
} else {
|
||||
const usernameAvailable = this.state.username &&
|
||||
this.state.usernameCheckSupport && !this.state.usernameError;
|
||||
@@ -267,25 +267,22 @@ export default React.createClass({
|
||||
</div>
|
||||
{ usernameIndicator }
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'This will be your account name on the <span></span> ' +
|
||||
'homeserver, or you can pick a <a>different server</a>.',
|
||||
[
|
||||
/<span><\/span>/,
|
||||
/<a>(.*?)<\/a>/,
|
||||
],
|
||||
[
|
||||
(sub) => <span>{this.props.homeserverUrl}</span>,
|
||||
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
|
||||
],
|
||||
)}
|
||||
{},
|
||||
{
|
||||
'span': <span>{ this.props.homeserverUrl }</span>,
|
||||
'a': (sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
<p>
|
||||
{ _tJsx(
|
||||
{ _t(
|
||||
'If you already have a Matrix account you can <a>log in</a> instead.',
|
||||
/<a>(.*?)<\/a>/,
|
||||
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
|
||||
)}
|
||||
{},
|
||||
{ 'a': (sub) => <a href="#" onClick={this.props.onLoginClick}>{ sub }</a> },
|
||||
) }
|
||||
</p>
|
||||
{ auth }
|
||||
{ authErrorIndicator }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user